mirror of https://github.com/kortix-ai/suna.git
194 lines
7.0 KiB
TypeScript
194 lines
7.0 KiB
TypeScript
import React from "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";
|
|
|
|
export function ExposePortToolView({
|
|
name = 'expose-port',
|
|
assistantContent,
|
|
toolContent,
|
|
isSuccess = true,
|
|
isStreaming = false,
|
|
assistantTimestamp,
|
|
toolTimestamp
|
|
}: ToolViewProps) {
|
|
console.log('ExposePortToolView:', {
|
|
name,
|
|
assistantContent,
|
|
toolContent,
|
|
isSuccess,
|
|
isStreaming,
|
|
assistantTimestamp,
|
|
toolTimestamp
|
|
});
|
|
|
|
// Parse the assistant content
|
|
const parsedAssistantContent = React.useMemo(() => {
|
|
if (!assistantContent) return null;
|
|
try {
|
|
const parsed = JSON.parse(assistantContent);
|
|
return parsed.content;
|
|
} catch (e) {
|
|
console.error('Failed to parse assistant content:', e);
|
|
return null;
|
|
}
|
|
}, [assistantContent]);
|
|
|
|
// Parse the tool result
|
|
const toolResult = React.useMemo(() => {
|
|
if (!toolContent) return null;
|
|
try {
|
|
// First parse the outer JSON
|
|
const parsed = JSON.parse(toolContent);
|
|
// Then extract the tool result content
|
|
const match = parsed.content.match(/output='(.*?)'/);
|
|
if (match) {
|
|
const jsonStr = match[1]
|
|
.replace(/\\n/g, '')
|
|
.replace(/\\"/g, '"');
|
|
return JSON.parse(jsonStr);
|
|
}
|
|
return null;
|
|
} catch (e) {
|
|
console.error('Failed to parse tool content:', e);
|
|
return null;
|
|
}
|
|
}, [toolContent]);
|
|
|
|
// Extract port number from assistant content
|
|
const portNumber = React.useMemo(() => {
|
|
if (!parsedAssistantContent) return null;
|
|
try {
|
|
const match = parsedAssistantContent.match(/<expose-port>\s*(\d+)\s*<\/expose-port>/);
|
|
return match ? match[1] : null;
|
|
} catch (e) {
|
|
console.error('Failed to extract port number:', e);
|
|
return null;
|
|
}
|
|
}, [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>
|
|
);
|
|
}
|
|
|
|
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>
|
|
)}
|
|
</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>
|
|
</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">
|
|
{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 ? 'Port exposed successfully' : 'Failed to expose port'}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{isStreaming && (
|
|
<div className="flex items-center gap-2">
|
|
<span>Exposing port...</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-xs">
|
|
{toolTimestamp && !isStreaming
|
|
? formatTimestamp(toolTimestamp)
|
|
: assistantTimestamp
|
|
? formatTimestamp(assistantTimestamp)
|
|
: ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|