From c9b16c9330e84756aac7f57c021c6632b8d7d379 Mon Sep 17 00:00:00 2001 From: Vukasin Date: Thu, 5 Jun 2025 20:18:12 +0200 Subject: [PATCH] feat: deploy tool --- .../thread/tool-views/DeployToolView.tsx | 283 ++++++++++++++++++ .../src/components/thread/tool-views/utils.ts | 7 +- .../tool-views/wrapper/ToolViewRegistry.tsx | 5 +- 3 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/thread/tool-views/DeployToolView.tsx diff --git a/frontend/src/components/thread/tool-views/DeployToolView.tsx b/frontend/src/components/thread/tool-views/DeployToolView.tsx new file mode 100644 index 00000000..22570d68 --- /dev/null +++ b/frontend/src/components/thread/tool-views/DeployToolView.tsx @@ -0,0 +1,283 @@ +import React from 'react'; +import { + Rocket, + CheckCircle, + AlertTriangle, + ExternalLink, + Globe, + Folder, + TerminalIcon, +} from 'lucide-react'; +import { ToolViewProps } from './types'; +import { getToolTitle, normalizeContentToString } from './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 { ScrollArea } from "@/components/ui/scroll-area"; +import { LoadingState } from './shared/LoadingState'; + +interface DeployResult { + message?: string; + output?: string; + success?: boolean; + url?: string; +} + +function extractDeployData(assistantContent: any, toolContent: any): { + name: string | null; + directoryPath: string | null; + deployResult: DeployResult | null; + rawContent: string | null; +} { + let name: string | null = null; + let directoryPath: string | null = null; + let deployResult: DeployResult | null = null; + let rawContent: string | null = null; + + // Try to extract from assistant content first + const assistantStr = normalizeContentToString(assistantContent); + if (assistantStr) { + try { + const parsed = JSON.parse(assistantStr); + if (parsed.parameters) { + name = parsed.parameters.name || null; + directoryPath = parsed.parameters.directory_path || null; + } + } catch (e) { + // Try regex extraction + const nameMatch = assistantStr.match(/name["']\s*:\s*["']([^"']+)["']/); + const dirMatch = assistantStr.match(/directory_path["']\s*:\s*["']([^"']+)["']/); + if (nameMatch) name = nameMatch[1]; + if (dirMatch) directoryPath = dirMatch[1]; + } + } + + // Extract deploy result from tool content + const toolStr = normalizeContentToString(toolContent); + if (toolStr) { + rawContent = toolStr; + try { + const parsed = JSON.parse(toolStr); + + // Handle the nested tool_execution structure + let resultData = null; + if (parsed.tool_execution && parsed.tool_execution.result) { + resultData = parsed.tool_execution.result; + // Also extract arguments if not found in assistant content + if (!name && parsed.tool_execution.arguments) { + name = parsed.tool_execution.arguments.name || null; + directoryPath = parsed.tool_execution.arguments.directory_path || null; + } + } else if (parsed.output) { + // Fallback to old format + resultData = parsed; + } + + if (resultData) { + deployResult = { + message: resultData.output?.message || null, + output: resultData.output?.output || null, + success: resultData.success !== undefined ? resultData.success : true, + }; + + // Try to extract deployment URL from output + if (deployResult.output) { + const urlMatch = deployResult.output.match(/https:\/\/[^\s]+\.pages\.dev[^\s]*/); + if (urlMatch) { + deployResult.url = urlMatch[0]; + } + } + } + } catch (e) { + // If parsing fails, treat as raw content + deployResult = { + message: 'Deploy completed', + output: toolStr, + success: true, + }; + } + } + + return { name, directoryPath, deployResult, rawContent }; +} + +export function DeployToolView({ + name = 'deploy', + assistantContent, + toolContent, + assistantTimestamp, + toolTimestamp, + isSuccess = true, + isStreaming = false, +}: ToolViewProps) { + const { name: projectName, directoryPath, deployResult, rawContent } = extractDeployData( + assistantContent, + toolContent + ); + + const toolTitle = getToolTitle(name); + const actualIsSuccess = deployResult?.success !== undefined ? deployResult.success : isSuccess; + + // Clean up terminal output for display + const cleanOutput = React.useMemo(() => { + if (!deployResult?.output) return []; + + let output = deployResult.output; + // Remove ANSI escape codes + output = output.replace(/\u001b\[[0-9;]*m/g, ''); + // Replace escaped newlines with actual newlines + output = output.replace(/\\n/g, '\n'); + + return output.split('\n').filter(line => line.trim().length > 0); + }, [deployResult?.output]); + + return ( + + +
+
+
+ +
+
+ + {toolTitle} + +
+
+ + {!isStreaming && ( + + {actualIsSuccess ? ( + + ) : ( + + )} + {actualIsSuccess ? 'Deploy successful' : 'Deploy failed'} + + )} +
+
+ + + {isStreaming ? ( + + ) : ( + +
+ + {/* Success State */} + {actualIsSuccess && deployResult ? ( +
+ {/* Deployment URL Card */} + {deployResult.url && ( +
+
+
+
+ +
+ + Website Deployed + + + Live + +
+ +
+ + {deployResult.url} + +
+ + +
+
+ )} + + {/* Terminal Output */} + {cleanOutput.length > 0 && ( +
+
+ + + Deployment Log + +
+
+
+                                                    {cleanOutput.map((line, index) => (
+                                                        
+ {line || ' '} +
+ ))} +
+
+
+ )} +
+ ) : ( + /* Failure State */ +
+
+
+ +

+ Deployment Failed +

+
+

+ The deployment encountered an error. Check the logs below for details. +

+
+ + {/* Raw Error Output */} + {rawContent && ( +
+
+ + + Error Details + +
+
+
+                                                    {rawContent}
+                                                
+
+
+ )} +
+ )} +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/tool-views/utils.ts b/frontend/src/components/thread/tool-views/utils.ts index dca1c5ec..8986c71f 100644 --- a/frontend/src/components/thread/tool-views/utils.ts +++ b/frontend/src/components/thread/tool-views/utils.ts @@ -53,7 +53,7 @@ export function getToolTitle(toolName: string): string { 'complete': 'Task Complete', 'execute-data-provider-call': 'Data Provider Call', 'get-data-provider-endpoints': 'Data Endpoints', - + 'deploy': 'Deploy', 'generic-tool': 'Tool', 'default': 'Tool', @@ -1232,6 +1232,11 @@ export function getToolComponent(toolName: string): string { case 'get-data-provider-endpoints': return 'DataProviderToolView'; + + //Deploy + case 'deploy': + return 'DeployToolView'; + // Default default: return 'GenericToolView'; diff --git a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx index 4b7cd271..6ebd3880 100644 --- a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx +++ b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx @@ -15,6 +15,7 @@ import { AskToolView } from '../ask-tool/AskToolView'; import { CompleteToolView } from '../CompleteToolView'; import { ExecuteDataProviderCallToolView } from '../data-provider-tool/ExecuteDataProviderCallToolView'; import { DataProviderEndpointsToolView } from '../data-provider-tool/DataProviderEndpointsToolView'; +import { DeployToolView } from '../DeployToolView'; export type ToolViewComponent = React.ComponentType; @@ -60,12 +61,14 @@ const defaultRegistry: ToolViewRegistryType = { 'expose-port': ExposePortToolView, 'see-image': SeeImageToolView, - + 'call-mcp-tool': GenericToolView, 'ask': AskToolView, 'complete': CompleteToolView, + 'deploy': DeployToolView, + 'default': GenericToolView, };