Spaces:
Running
Running
File size: 7,193 Bytes
67c7241 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
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>
);
} |