mirror of https://github.com/kortix-ai/suna.git
chore(ui): tool-panel ui revamp
This commit is contained in:
parent
6fc03aba04
commit
67604ea95a
|
@ -118,4 +118,4 @@ export function ThreadSkeleton({
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -6,6 +6,9 @@ import {
|
|||
CheckCircle,
|
||||
AlertTriangle,
|
||||
CircleDashed,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Loader2
|
||||
} from 'lucide-react';
|
||||
import { ToolViewProps } from './types';
|
||||
import {
|
||||
|
@ -17,6 +20,12 @@ import {
|
|||
import { ApiMessageType } from '@/components/thread/types';
|
||||
import { safeJsonParse } from '@/components/thread/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
export function BrowserToolView({
|
||||
name = 'browser-operation',
|
||||
|
@ -84,7 +93,6 @@ export function BrowserToolView({
|
|||
browserStateMessage.content,
|
||||
{},
|
||||
);
|
||||
console.log('Browser state content: ', browserStateContent)
|
||||
screenshotBase64 = browserStateContent?.screenshot_base64 || null;
|
||||
}
|
||||
}
|
||||
|
@ -110,120 +118,180 @@ export function BrowserToolView({
|
|||
<iframe
|
||||
src={vncPreviewUrl}
|
||||
title="Browser preview"
|
||||
className="w-full h-full border-0 flex-1"
|
||||
className="w-full h-full border-0 min-h-[600px]"
|
||||
style={{ width: '100%', height: '100%', minHeight: '600px' }}
|
||||
/>
|
||||
);
|
||||
}, [vncPreviewUrl]); // Only recreate if the URL changes
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col">
|
||||
<div className="bg-zinc-100 dark:bg-zinc-900 p-2 flex items-center justify-between border-b border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center">
|
||||
<MonitorPlay className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" />
|
||||
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
|
||||
Browser Window
|
||||
</span>
|
||||
</div>
|
||||
{url && (
|
||||
<div className="text-xs font-mono text-zinc-500 dark:text-zinc-400 truncate max-w-[340px]">
|
||||
{url}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
// Progress for loading state - only for visual feedback
|
||||
const [progress, setProgress] = React.useState(100);
|
||||
|
||||
{/* Preview Logic */}
|
||||
<div className="flex-1 flex items-stretch bg-black">
|
||||
{isLastToolCall ? (
|
||||
// Only show live sandbox or fallback to sandbox for the last tool call
|
||||
isRunning && vncIframe ? (
|
||||
// Use the memoized iframe for live preview
|
||||
vncIframe
|
||||
) : screenshotBase64 ? (
|
||||
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${screenshotBase64}`}
|
||||
alt="Browser Screenshot"
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : vncIframe ? (
|
||||
// Use the memoized iframe
|
||||
vncIframe
|
||||
// Simulate progress when running
|
||||
React.useEffect(() => {
|
||||
if (isRunning) {
|
||||
setProgress(0);
|
||||
const timer = setInterval(() => {
|
||||
setProgress((prevProgress) => {
|
||||
if (prevProgress >= 95) {
|
||||
clearInterval(timer);
|
||||
return prevProgress;
|
||||
}
|
||||
return prevProgress + 2;
|
||||
});
|
||||
}, 500);
|
||||
return () => clearInterval(timer);
|
||||
} else {
|
||||
setProgress(100);
|
||||
}
|
||||
}, [isRunning]);
|
||||
|
||||
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-white dark:bg-zinc-950">
|
||||
<CardHeader className="h-13 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="p-2 rounded-lg bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
|
||||
<MonitorPlay className="h-5 w-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>
|
||||
|
||||
{!isRunning && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
isSuccess
|
||||
? "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"
|
||||
}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
||||
) : (
|
||||
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400">
|
||||
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
|
||||
<p className="text-sm font-medium">
|
||||
Browser preview not available
|
||||
</p>
|
||||
{url && (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 flex items-center text-blue-600 dark:text-blue-500 hover:text-blue-500 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
Visit URL <ExternalLink className="h-3 w-3 ml-1" />
|
||||
</a>
|
||||
)}
|
||||
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
{isSuccess ? 'Browser action completed' : 'Browser action failed'}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{isRunning && (
|
||||
<Badge className="bg-gradient-to-b from-blue-200 to-blue-100 text-blue-700 dark:from-blue-800/50 dark:to-blue-900/60 dark:text-blue-300">
|
||||
<CircleDashed className="h-3.5 w-3.5 mr-1 animate-spin" />
|
||||
Executing browser action
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0 flex-1 overflow-hidden relative" style={{ height: 'calc(100vh - 150px)', minHeight: '600px' }}>
|
||||
{/* Browser View Logic - Preserving the core functionality */}
|
||||
<div className="flex-1 flex h-full items-stretch bg-black">
|
||||
{isLastToolCall ? (
|
||||
// Only show live sandbox or fallback to screenshot for the last tool call
|
||||
isRunning && vncIframe ? (
|
||||
<div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
|
||||
<div className="relative w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
|
||||
{vncIframe}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<Badge className="bg-blue-500/90 text-white border-none shadow-lg animate-pulse">
|
||||
<CircleDashed className="h-3 w-3 mr-1.5 animate-spin" />
|
||||
{operation} in progress
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State image found"
|
||||
screenshotBase64 ? (
|
||||
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
|
||||
</div>
|
||||
) : screenshotBase64 ? (
|
||||
<div className="flex items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${screenshotBase64}`}
|
||||
alt="Browser Screenshot"
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 flex flex-col items-center justify-center w-full bg-zinc-50 dark:bg-zinc-900 text-zinc-700 dark:text-zinc-400">
|
||||
<MonitorPlay className="h-12 w-12 mb-3 opacity-40" />
|
||||
<p className="text-sm font-medium">
|
||||
No Browser State image found
|
||||
</p>
|
||||
) : vncIframe ? (
|
||||
// Use the memoized iframe
|
||||
<div className="flex flex-col items-center justify-center w-full h-full min-h-[600px]" style={{ minHeight: '600px' }}>
|
||||
{vncIframe}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 flex flex-col items-center justify-center w-full bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900 text-zinc-700 dark:text-zinc-400">
|
||||
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60">
|
||||
<MonitorPlay className="h-10 w-10 text-purple-400 dark:text-purple-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
|
||||
Browser preview not available
|
||||
</h3>
|
||||
{url && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-700 shadow-sm hover:shadow-md transition-shadow"
|
||||
asChild
|
||||
>
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||
Visit URL
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : // For non-last tool calls, only show screenshot if available, otherwise show "No Browser State"
|
||||
screenshotBase64 ? (
|
||||
<div className="flex items-center justify-center w-full h-full max-h-[650px] overflow-auto">
|
||||
<img
|
||||
src={`data:image/jpeg;base64,${screenshotBase64}`}
|
||||
alt="Browser Screenshot"
|
||||
className="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8 h-full flex flex-col items-center justify-center w-full bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900 text-zinc-700 dark:text-zinc-400">
|
||||
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
|
||||
<MonitorPlay className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
|
||||
No Browser State Available
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||
Browser state image not found for this action
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400">
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{!isRunning && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
||||
)}
|
||||
<span>
|
||||
{isSuccess
|
||||
? `${operation} completed successfully`
|
||||
: `${operation} failed`}
|
||||
</span>
|
||||
</div>
|
||||
<Badge className="h-6 py-0.5">
|
||||
<Globe className="h-3 w-3" />
|
||||
{operation}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" />
|
||||
<span>Executing browser action...</span>
|
||||
</div>
|
||||
{url && (
|
||||
<span className="text-xs truncate max-w-[200px] hidden sm:inline-block">
|
||||
{url}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="text-xs">
|
||||
{toolTimestamp && !isRunning
|
||||
? formatTimestamp(toolTimestamp)
|
||||
: assistantTimestamp
|
||||
? formatTimestamp(assistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{toolTimestamp && !isRunning
|
||||
? formatTimestamp(toolTimestamp)
|
||||
: assistantTimestamp
|
||||
? formatTimestamp(assistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,19 +1,30 @@
|
|||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Terminal,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
CircleDashed,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Clock,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Loader2,
|
||||
ArrowRight
|
||||
} from 'lucide-react';
|
||||
import { ToolViewProps } from './types';
|
||||
import {
|
||||
extractCommand,
|
||||
extractCommandOutput,
|
||||
extractExitCode,
|
||||
formatTimestamp,
|
||||
getToolTitle,
|
||||
} from './utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
export function CommandToolView({
|
||||
name = 'execute-command',
|
||||
|
@ -24,15 +35,17 @@ export function CommandToolView({
|
|||
isSuccess = true,
|
||||
isStreaming = false,
|
||||
}: ToolViewProps) {
|
||||
// Extract command with improved XML parsing
|
||||
const { resolvedTheme } = useTheme();
|
||||
const isDarkTheme = resolvedTheme === 'dark';
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [showFullOutput, setShowFullOutput] = useState(false);
|
||||
|
||||
const rawCommand = React.useMemo(() => {
|
||||
if (!assistantContent) return null;
|
||||
|
||||
try {
|
||||
// Try to parse JSON content first
|
||||
const parsed = JSON.parse(assistantContent);
|
||||
if (parsed.content) {
|
||||
// Look for execute-command tag
|
||||
const commandMatch = parsed.content.match(
|
||||
/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/,
|
||||
);
|
||||
|
@ -41,7 +54,6 @@ export function CommandToolView({
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, try direct XML extraction
|
||||
const commandMatch = assistantContent.match(
|
||||
/<execute-command[^>]*>([\s\S]*?)<\/execute-command>/,
|
||||
);
|
||||
|
@ -53,198 +65,286 @@ export function CommandToolView({
|
|||
return null;
|
||||
}, [assistantContent]);
|
||||
|
||||
// Clean the command by removing any leading/trailing whitespace and newlines
|
||||
const command = rawCommand
|
||||
?.replace(/^suna@computer:~\$\s*/g, '') // Remove prompt prefix
|
||||
?.replace(/\\n/g, '') // Remove escaped newlines
|
||||
?.replace(/\n/g, '') // Remove actual newlines
|
||||
?.trim(); // Clean up any remaining whitespace
|
||||
?.replace(/^suna@computer:~\$\s*/g, '')
|
||||
?.replace(/\\n/g, '')
|
||||
?.replace(/\n/g, '')
|
||||
?.trim();
|
||||
|
||||
// Extract and clean the output with improved parsing
|
||||
const output = React.useMemo(() => {
|
||||
if (!toolContent) return null;
|
||||
|
||||
let extractedOutput = '';
|
||||
let success = true;
|
||||
|
||||
try {
|
||||
// Try to parse JSON content first
|
||||
const parsed = JSON.parse(toolContent);
|
||||
if (parsed.content) {
|
||||
// Look for tool_result tag
|
||||
const toolResultMatch = parsed.content.match(
|
||||
/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/,
|
||||
);
|
||||
if (toolResultMatch) {
|
||||
return toolResultMatch[1].trim();
|
||||
}
|
||||
|
||||
// Look for output field in a ToolResult pattern
|
||||
const outputMatch = parsed.content.match(
|
||||
/ToolResult\(.*?output='([\s\S]*?)'.*?\)/,
|
||||
);
|
||||
if (outputMatch) {
|
||||
return outputMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
||||
}
|
||||
|
||||
// Try to parse as direct JSON
|
||||
try {
|
||||
const outputJson = JSON.parse(parsed.content);
|
||||
if (outputJson.output) {
|
||||
return outputJson.output;
|
||||
if (typeof toolContent === 'string') {
|
||||
if (toolContent.includes('ToolResult')) {
|
||||
const successMatch = toolContent.match(/success=(true|false)/i);
|
||||
success = successMatch ? successMatch[1].toLowerCase() === 'true' : true;
|
||||
|
||||
//@ts-expect-error IGNORE
|
||||
const outputMatch = toolContent.match(/output=['"](.*)['"]/s);
|
||||
if (outputMatch && outputMatch[1]) {
|
||||
extractedOutput = outputMatch[1]
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\'/g, "'");
|
||||
} else {
|
||||
extractedOutput = toolContent;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const parsed = JSON.parse(toolContent);
|
||||
if (parsed.output) {
|
||||
extractedOutput = parsed.output;
|
||||
success = parsed.success !== false;
|
||||
} else if (parsed.content) {
|
||||
extractedOutput = parsed.content;
|
||||
} else {
|
||||
extractedOutput = JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
extractedOutput = toolContent;
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, use the content as-is
|
||||
return parsed.content;
|
||||
}
|
||||
} else {
|
||||
extractedOutput = String(toolContent);
|
||||
}
|
||||
} catch (e) {
|
||||
// If JSON parsing fails, try direct XML extraction
|
||||
const toolResultMatch = toolContent.match(
|
||||
/<tool_result>\s*<execute-command>([\s\S]*?)<\/execute-command>\s*<\/tool_result>/,
|
||||
);
|
||||
if (toolResultMatch) {
|
||||
return toolResultMatch[1].trim();
|
||||
}
|
||||
|
||||
const outputMatch = toolContent.match(
|
||||
/ToolResult\(.*?output='([\s\S]*?)'.*?\)/,
|
||||
);
|
||||
if (outputMatch) {
|
||||
return outputMatch[1].replace(/\\n/g, '\n').replace(/\\"/g, '"');
|
||||
}
|
||||
extractedOutput = String(toolContent);
|
||||
console.error('Error parsing tool content:', e);
|
||||
}
|
||||
|
||||
return toolContent;
|
||||
return extractedOutput;
|
||||
}, [toolContent]);
|
||||
|
||||
const exitCode = extractExitCode(toolContent);
|
||||
const exitCode = extractExitCode(output);
|
||||
const toolTitle = getToolTitle(name);
|
||||
|
||||
// Simulate progress when streaming
|
||||
useEffect(() => {
|
||||
if (isStreaming) {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((prevProgress) => {
|
||||
if (prevProgress >= 95) {
|
||||
clearInterval(timer);
|
||||
return prevProgress;
|
||||
}
|
||||
return prevProgress + 5;
|
||||
});
|
||||
}, 300);
|
||||
return () => clearInterval(timer);
|
||||
} else {
|
||||
setProgress(100);
|
||||
}
|
||||
}, [isStreaming]);
|
||||
|
||||
// Format and handle the output for display
|
||||
const formattedOutput = React.useMemo(() => {
|
||||
if (!output) return [];
|
||||
|
||||
// Replace JSON string escaped newlines with actual newlines
|
||||
let processedOutput = output
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\'/g, "'");
|
||||
|
||||
// Split by real newlines for line-by-line display
|
||||
return processedOutput.split('\n');
|
||||
}, [output]);
|
||||
|
||||
// Only show a preview if there are many lines
|
||||
const hasMoreLines = formattedOutput.length > 10;
|
||||
const previewLines = formattedOutput.slice(0, 10);
|
||||
const linesToShow = showFullOutput ? formattedOutput : previewLines;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="border border-zinc-200 dark:border-zinc-800 rounded-md overflow-hidden h-full flex flex-col">
|
||||
<div className="flex items-center p-2 bg-zinc-100 dark:bg-zinc-900 justify-between border-b border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center">
|
||||
<Terminal className="h-4 w-4 mr-2 text-zinc-600 dark:text-zinc-400" />
|
||||
<span className="text-xs font-medium text-zinc-700 dark:text-zinc-300">
|
||||
Terminal
|
||||
</span>
|
||||
<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-white dark:bg-zinc-950">
|
||||
<CardHeader className="h-13 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="p-2 rounded-lg bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
|
||||
<Terminal className="h-5 w-5 text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
{exitCode !== null && !isStreaming && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs flex items-center',
|
||||
isSuccess
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-red-600 dark:text-red-400',
|
||||
)}
|
||||
>
|
||||
<span className="h-1.5 w-1.5 rounded-full mr-1.5 bg-current"></span>
|
||||
Exit: {exitCode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="terminal-container flex-1 overflow-auto bg-black text-zinc-300 font-mono">
|
||||
<div className="p-3 text-xs">
|
||||
{command && output && !isStreaming && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start">
|
||||
<span className="text-emerald-400 shrink-0 mr-2">
|
||||
suna@computer:~$
|
||||
</span>
|
||||
<span className="text-zinc-300">{command}</span>
|
||||
</div>
|
||||
|
||||
<div className="whitespace-pre-wrap break-words text-zinc-400 pl-0">
|
||||
{output}
|
||||
</div>
|
||||
|
||||
{isSuccess && (
|
||||
<div className="text-emerald-400 mt-1">
|
||||
suna@computer:~$ _
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{command && !output && !isStreaming && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start">
|
||||
<span className="text-emerald-400 shrink-0 mr-2">
|
||||
suna@computer:~$
|
||||
</span>
|
||||
<span className="text-zinc-300">{command}</span>
|
||||
</div>
|
||||
<div className="flex items-center h-4">
|
||||
<div className="w-2 h-4 bg-zinc-500 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!command && !output && !isStreaming && (
|
||||
<div className="flex items-start">
|
||||
<span className="text-emerald-400 shrink-0 mr-2">
|
||||
suna@computer:~$
|
||||
</span>
|
||||
<span className="w-2 h-4 bg-zinc-500 animate-pulse"></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStreaming && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start">
|
||||
<span className="text-emerald-400 shrink-0 mr-2">
|
||||
suna@computer:~$
|
||||
</span>
|
||||
<span className="text-zinc-300">
|
||||
{command || 'running command...'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-zinc-400">
|
||||
<CircleDashed className="h-3 w-3 animate-spin text-blue-400" />
|
||||
<span>Command execution in progress...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{toolTitle}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400">
|
||||
|
||||
{!isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
isSuccess
|
||||
? "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"
|
||||
}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" />
|
||||
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
||||
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
<span>
|
||||
{isSuccess
|
||||
? `Command completed successfully${exitCode !== null ? ` (exit code: ${exitCode})` : ''}`
|
||||
: `Command failed${exitCode !== null ? ` with exit code ${exitCode}` : ''}`}
|
||||
</span>
|
||||
</div>
|
||||
{isSuccess ? 'Command executed successfully' : 'Command failed'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleDashed className="h-3.5 w-3.5 text-blue-500 animate-spin" />
|
||||
<span>Executing command...</span>
|
||||
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
|
||||
{isStreaming ? (
|
||||
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
|
||||
<div className="text-center w-full max-w-xs">
|
||||
<div className="w-16 h-16 rounded-full mx-auto mb-6 flex items-center justify-center bg-gradient-to-b from-purple-100 to-purple-50 shadow-inner dark:from-purple-800/40 dark:to-purple-900/60 dark:shadow-purple-950/20">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-purple-500 dark:text-purple-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
Executing command
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
|
||||
<span className="font-mono text-xs break-all">{command || 'Processing command...'}</span>
|
||||
</p>
|
||||
<Progress value={progress} className="w-full h-2" />
|
||||
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-2">{progress}%</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs">
|
||||
{toolTimestamp && !isStreaming
|
||||
? formatTimestamp(toolTimestamp)
|
||||
: assistantTimestamp
|
||||
? formatTimestamp(assistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
) : command ? (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-4">
|
||||
<div className="mb-4 bg-zinc-100 dark:bg-zinc-800/80 rounded-lg overflow-hidden shadow-sm border border-zinc-200 dark:border-zinc-800">
|
||||
<div className="bg-zinc-200 dark:bg-zinc-800 px-4 py-2 flex items-center gap-2">
|
||||
<Code className="h-4 w-4 text-zinc-600 dark:text-zinc-400" />
|
||||
<span className="text-sm font-medium text-zinc-700 dark:text-zinc-300">Command</span>
|
||||
</div>
|
||||
<div className="p-4 font-mono text-sm text-zinc-700 dark:text-zinc-300 flex gap-2">
|
||||
<span className="text-purple-500 dark:text-purple-400 select-none">$</span>
|
||||
<code className="flex-1 break-all">{command}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{output && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="text-sm font-medium text-zinc-700 dark:text-zinc-300 flex items-center">
|
||||
<ArrowRight className="h-4 w-4 mr-2 text-zinc-500 dark:text-zinc-400" />
|
||||
Output
|
||||
{exitCode !== null && (
|
||||
<Badge
|
||||
className={cn(
|
||||
"ml-2",
|
||||
exitCode === 0
|
||||
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
)}
|
||||
>
|
||||
Exit code: {exitCode}
|
||||
</Badge>
|
||||
)}
|
||||
</h3>
|
||||
{hasMoreLines && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowFullOutput(!showFullOutput)}
|
||||
className="h-7 text-xs flex items-center gap-1"
|
||||
>
|
||||
{showFullOutput ? (
|
||||
<>
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
Show less
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
Show full output
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-100 dark:bg-neutral-900 rounded-lg overflow-hidden border border-zinc-00/20">
|
||||
<div className="bg-zinc-300 dark:bg-neutral-800 px-3 py-1 flex items-center justify-between border-b border-zinc-300/50 dark:border-zinc-700/50">
|
||||
<span className="text-xs text-zinc-600 dark:text-zinc-400">Terminal output</span>
|
||||
{exitCode !== null && exitCode !== 0 && (
|
||||
<Badge variant="outline" className="text-xs h-5 border-red-700/30 text-red-400">
|
||||
<AlertTriangle className="h-3 w-3 mr-1" />
|
||||
Error
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4 max-h-96 overflow-auto scrollbar-hide">
|
||||
<pre className="text-xs text-zinc-600 dark:text-zinc-300 font-mono whitespace-pre-wrap break-all overflow-visible">
|
||||
{linesToShow.map((line, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"py-0.5 bg-transparent",
|
||||
)}
|
||||
>
|
||||
{line || ' '}
|
||||
</div>
|
||||
))}
|
||||
{!showFullOutput && hasMoreLines && (
|
||||
<div className="text-zinc-500 mt-2 border-t border-zinc-700/30 pt-2">
|
||||
+ {formattedOutput.length - 10} more lines
|
||||
</div>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!output && !isStreaming && (
|
||||
<div className="bg-black rounded-lg overflow-hidden border border-zinc-700/20 shadow-md p-6 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<CircleDashed className="h-8 w-8 text-zinc-500 mx-auto mb-2" />
|
||||
<p className="text-zinc-400 text-sm">No output received</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
|
||||
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
|
||||
<Terminal className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
|
||||
No Command Found
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center max-w-md">
|
||||
No command was detected. Please provide a valid command to execute.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{!isStreaming && command && (
|
||||
<Badge variant="outline" className="h-6 py-0.5 bg-zinc-50 dark:bg-zinc-900">
|
||||
<Terminal className="h-3 w-3 mr-1" />
|
||||
Command
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 flex items-center gap-2">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
{toolTimestamp && !isStreaming
|
||||
? formatTimestamp(toolTimestamp)
|
||||
: assistantTimestamp
|
||||
? formatTimestamp(assistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo, useState, useEffect } from 'react';
|
||||
import {
|
||||
ExternalLink,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
Globe,
|
||||
Loader2,
|
||||
Link2,
|
||||
Computer
|
||||
} from 'lucide-react';
|
||||
import { ToolViewProps } from './types';
|
||||
import { formatTimestamp } from './utils';
|
||||
import { ExternalLink, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||
import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function ExposePortToolView({
|
||||
name = 'expose-port',
|
||||
|
@ -14,18 +27,10 @@ export function ExposePortToolView({
|
|||
assistantTimestamp,
|
||||
toolTimestamp,
|
||||
}: ToolViewProps) {
|
||||
console.log('ExposePortToolView:', {
|
||||
name,
|
||||
assistantContent,
|
||||
toolContent,
|
||||
isSuccess,
|
||||
isStreaming,
|
||||
assistantTimestamp,
|
||||
toolTimestamp,
|
||||
});
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
// Parse the assistant content
|
||||
const parsedAssistantContent = React.useMemo(() => {
|
||||
const parsedAssistantContent = useMemo(() => {
|
||||
if (!assistantContent) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(assistantContent);
|
||||
|
@ -37,7 +42,7 @@ export function ExposePortToolView({
|
|||
}, [assistantContent]);
|
||||
|
||||
// Parse the tool result
|
||||
const toolResult = React.useMemo(() => {
|
||||
const toolResult = useMemo(() => {
|
||||
if (!toolContent) return null;
|
||||
try {
|
||||
// First parse the outer JSON
|
||||
|
@ -56,7 +61,7 @@ export function ExposePortToolView({
|
|||
}, [toolContent]);
|
||||
|
||||
// Extract port number from assistant content
|
||||
const portNumber = React.useMemo(() => {
|
||||
const portNumber = useMemo(() => {
|
||||
if (!parsedAssistantContent) return null;
|
||||
try {
|
||||
const match = parsedAssistantContent.match(
|
||||
|
@ -69,141 +74,219 @@ export function ExposePortToolView({
|
|||
}
|
||||
}, [parsedAssistantContent]);
|
||||
|
||||
// If we have no content to show, render a placeholder
|
||||
if (!portNumber && !toolResult && !isStreaming) {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-4">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
No port exposure information available
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// Simulate progress when streaming
|
||||
useEffect(() => {
|
||||
if (isStreaming) {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((prevProgress) => {
|
||||
if (prevProgress >= 95) {
|
||||
clearInterval(timer);
|
||||
return prevProgress;
|
||||
}
|
||||
return prevProgress + 5;
|
||||
});
|
||||
}, 300);
|
||||
return () => clearInterval(timer);
|
||||
} else {
|
||||
setProgress(100);
|
||||
}
|
||||
}, [isStreaming]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
{/* Assistant Content */}
|
||||
{portNumber && !isStreaming && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
Port to Expose
|
||||
</div>
|
||||
{assistantTimestamp && (
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{formatTimestamp(assistantTimestamp)}
|
||||
</div>
|
||||
)}
|
||||
<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-white dark:bg-zinc-950">
|
||||
<CardHeader className="h-13 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="p-2 rounded-lg bg-gradient-to-b from-emerald-100 to-emerald-50 shadow-inner dark:from-emerald-800/40 dark:to-emerald-900/60 dark:shadow-emerald-950/20">
|
||||
<Computer className="h-5 w-5 text-emerald-500 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div className="rounded-md border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-xs font-medium text-zinc-800 dark:text-zinc-300">
|
||||
Port
|
||||
</div>
|
||||
<div className="px-2 py-1 rounded-md bg-zinc-100 dark:bg-zinc-800 text-xs font-mono text-zinc-800 dark:text-zinc-300">
|
||||
{portNumber}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
Port Exposure
|
||||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Result */}
|
||||
{toolResult && (
|
||||
<div className="space-y-1.5 mt-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
{isStreaming ? 'Processing' : 'Exposed URL'}
|
||||
</div>
|
||||
{toolTimestamp && !isStreaming && (
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{formatTimestamp(toolTimestamp)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-md border p-3',
|
||||
isStreaming
|
||||
? 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/10'
|
||||
: isSuccess
|
||||
? 'border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900'
|
||||
: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/10',
|
||||
)}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-blue-700 dark:text-blue-400">
|
||||
<span>Exposing port {portNumber}...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ExternalLink className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||
<a
|
||||
href={toolResult.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline break-all"
|
||||
>
|
||||
{toolResult.url}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
Port
|
||||
</div>
|
||||
<div className="px-2 py-1 rounded-md bg-zinc-100 dark:bg-zinc-800 text-xs font-mono text-zinc-800 dark:text-zinc-300">
|
||||
{toolResult.port}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-600 dark:text-zinc-400">
|
||||
{toolResult.message}
|
||||
</div>
|
||||
<div className="text-xs text-amber-600 dark:text-amber-400 italic">
|
||||
Note: This URL might only be temporarily available and could
|
||||
expire after some time.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-zinc-200 dark:border-zinc-800">
|
||||
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400">
|
||||
|
||||
{!isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
isSuccess
|
||||
? "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"
|
||||
}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 text-emerald-500" />
|
||||
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
||||
) : (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-red-500" />
|
||||
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
|
||||
)}
|
||||
<span>
|
||||
{isSuccess
|
||||
? 'Port exposed successfully'
|
||||
: 'Failed to expose port'}
|
||||
</span>
|
||||
</div>
|
||||
{isSuccess ? 'Port exposed successfully' : 'Failed to expose port'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{isStreaming && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Exposing port...</span>
|
||||
<CardContent className="p-0 h-full flex-1 overflow-hidden relative">
|
||||
{isStreaming ? (
|
||||
<div className="flex flex-col items-center justify-center h-full py-12 px-6 bg-gradient-to-b from-white to-zinc-50 dark:from-zinc-950 dark:to-zinc-900">
|
||||
<div className="text-center w-full max-w-xs">
|
||||
<div className="w-16 h-16 rounded-full mx-auto mb-6 flex items-center justify-center bg-gradient-to-b from-emerald-100 to-emerald-50 shadow-inner dark:from-emerald-800/40 dark:to-emerald-900/60 dark:shadow-emerald-950/20">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-emerald-500 dark:text-emerald-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-2">
|
||||
Exposing port
|
||||
</h3>
|
||||
<div className="mb-4">
|
||||
<Badge className="bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-300 text-xs font-mono px-3 py-1 rounded-md">
|
||||
{portNumber}
|
||||
</Badge>
|
||||
</div>
|
||||
<Progress value={progress} className="w-full h-2" />
|
||||
<p className="text-xs text-zinc-400 dark:text-zinc-500 mt-2">{progress}%</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs">
|
||||
{toolTimestamp && !isStreaming
|
||||
? formatTimestamp(toolTimestamp)
|
||||
: assistantTimestamp
|
||||
? formatTimestamp(assistantTimestamp)
|
||||
: ''}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-full w-full">
|
||||
<div className="p-4 py-0 my-4 space-y-6">
|
||||
{/* Port Information Section */}
|
||||
{portNumber && (
|
||||
<div className="bg-zinc-50/70 dark:bg-zinc-900/50 p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 shadow-sm">
|
||||
<h3 className="text-sm font-medium text-zinc-800 dark:text-zinc-300 mb-3 flex items-center">
|
||||
<Computer className="h-4 w-4 mr-2 opacity-70" />
|
||||
Port to Expose
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 bg-white dark:bg-zinc-800 p-3 rounded-md border border-zinc-200 dark:border-zinc-700 mb-1">
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">Port Number</div>
|
||||
<Badge className="bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border-none font-mono text-xs px-3 py-1">
|
||||
{portNumber}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400 mt-2">
|
||||
{assistantTimestamp && formatTimestamp(assistantTimestamp)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Exposed URL Section */}
|
||||
{toolResult && (
|
||||
<div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-sm overflow-hidden">
|
||||
<div className="p-4">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="p-2 rounded-lg bg-emerald-100 dark:bg-emerald-900/30">
|
||||
<Link2 className="h-4 w-4 text-emerald-600 dark:text-emerald-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium text-zinc-800 dark:text-zinc-200 mb-2">
|
||||
Exposed URL
|
||||
</h3>
|
||||
<a
|
||||
href={toolResult.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-md font-medium text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-2 mb-3"
|
||||
>
|
||||
{toolResult.url}
|
||||
<ExternalLink className="flex-shrink-0 h-3.5 w-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="text-xs font-medium text-zinc-500 dark:text-zinc-400">
|
||||
Port Details
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-zinc-50 dark:bg-zinc-800 font-mono">
|
||||
Port: {toolResult.port}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{toolResult.message}
|
||||
</div>
|
||||
|
||||
<div className="text-xs bg-amber-50 dark:bg-amber-950/30 border border-amber-100 dark:border-amber-900/50 rounded-md p-3 text-amber-600 dark:text-amber-400 flex items-start gap-2">
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0 mt-0.5" />
|
||||
<span>This URL might only be temporarily available and could expire after some time.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-50 dark:bg-zinc-800/50 border-t border-zinc-200 dark:border-zinc-800 p-3 flex justify-between items-center">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs bg-white dark:bg-zinc-900"
|
||||
asChild
|
||||
>
|
||||
<a href={toolResult.url} target="_blank" rel="noopener noreferrer">
|
||||
<Globe className="h-3 w-3 mr-1.5" />
|
||||
Open in Browser
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Open the exposed URL in a new tab</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{toolTimestamp && formatTimestamp(toolTimestamp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!portNumber && !toolResult && !isStreaming && (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-6">
|
||||
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-6 bg-gradient-to-b from-zinc-100 to-zinc-50 shadow-inner dark:from-zinc-800/40 dark:to-zinc-900/60">
|
||||
<Computer className="h-10 w-10 text-zinc-400 dark:text-zinc-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
|
||||
No Port Information
|
||||
</h3>
|
||||
<p className="text-sm text-zinc-500 dark:text-zinc-400 text-center max-w-md">
|
||||
No port exposure information is available yet. Use the expose-port command to share a local port.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
||||
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||
{!isStreaming && toolResult && (
|
||||
<Badge className="h-6 py-0.5 bg-emerald-100 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 border-none">
|
||||
<Computer className="h-3 w-3 mr-1" />
|
||||
Port {portNumber}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
||||
{isSuccess ? (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<CheckCircle className="h-3 w-3 text-emerald-500" />
|
||||
{isStreaming ? 'Exposing...' : 'Port exposed successfully'}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5">
|
||||
<AlertTriangle className="h-3 w-3 text-red-500" />
|
||||
Failed to expose port
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -432,39 +432,37 @@ export function FileOperationToolView({
|
|||
</Tabs>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader className="bg-gradient-to-r from-zinc-50/80 to-zinc-100/80 dark:from-zinc-900/80 dark:to-zinc-800/80 backdrop-blur-sm border-b border-zinc-200 dark:border-zinc-800 p-2 py-1 flex-row items-center justify-between space-y-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={cn("p-2 rounded-lg", config.bgColor)}>
|
||||
<Icon className={cn("h-5 w-5", config.color)} />
|
||||
<CardHeader className="h-13 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-3">
|
||||
<div className={cn("p-2 rounded-lg", config.bgColor)}>
|
||||
<Icon className={cn("h-5 w-5", config.color)} />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{processedFilePath || 'Unknown file path'}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{operation === 'create' ? 'Create File' :
|
||||
operation === 'rewrite' ? 'Update File' : 'Delete File'}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-sm text-zinc-500 dark:text-zinc-400 font-mono">
|
||||
{processedFilePath || 'Unknown file path'}
|
||||
</CardDescription>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!isStreaming ? (
|
||||
<Badge variant="secondary" className={cn("px-2 py-1 transition-colors", config.badgeColor)}>
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="mr-1.5 h-3.5 w-3.5" />
|
||||
) : (
|
||||
<AlertTriangle className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
{isSuccess ? config.successMessage : `Failed to ${operation}`}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300 px-2 py-1">
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{config.progressMessage}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{!isStreaming ? (
|
||||
<Badge variant="secondary" className={cn("px-2 py-1 transition-colors", config.badgeColor)}>
|
||||
{isSuccess ? (
|
||||
<CheckCircle className="mr-1.5 h-3.5 w-3.5" />
|
||||
) : (
|
||||
<AlertTriangle className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
{isSuccess ? config.successMessage : `Failed to ${operation}`}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300 px-2 py-1">
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
{config.progressMessage}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0 flex-1 overflow-hidden relative">
|
||||
|
|
|
@ -16,12 +16,21 @@ export type ToolViewComponent = React.ComponentType<ToolViewProps>;
|
|||
type ToolViewRegistryType = Record<string, ToolViewComponent>;
|
||||
|
||||
const defaultRegistry: ToolViewRegistryType = {
|
||||
'browser-navigate': BrowserToolView,
|
||||
'browser-click': BrowserToolView,
|
||||
'browser-extract': BrowserToolView,
|
||||
'browser-fill': BrowserToolView,
|
||||
'browser-navigate-to': BrowserToolView,
|
||||
'browser-go-back': BrowserToolView,
|
||||
'browser-wait': BrowserToolView,
|
||||
'browser-screenshot': BrowserToolView,
|
||||
'browser-click-element': BrowserToolView,
|
||||
'browser-input-text': BrowserToolView,
|
||||
'browser-send-keys': BrowserToolView,
|
||||
'browser-switch-tab': BrowserToolView,
|
||||
'browser-close-tab': BrowserToolView,
|
||||
'browser-scroll-down': BrowserToolView,
|
||||
'browser-scroll-up': BrowserToolView,
|
||||
'browser-scroll-to-text': BrowserToolView,
|
||||
'browser-get-dropdown-options': BrowserToolView,
|
||||
'browser-select-dropdown-option': BrowserToolView,
|
||||
'browser-drag-drop': BrowserToolView,
|
||||
'browser-click-coordinates': BrowserToolView,
|
||||
|
||||
'execute-command': CommandToolView,
|
||||
|
||||
|
|
Loading…
Reference in New Issue