tool-modal.tsx•8.89 kB
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Play, X, Copy, Clock, CheckCircle2 } from "lucide-react";
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@/lib/queryClient";
import { useToast } from "@/hooks/use-toast";
import LoadingSpinner from "@/components/ui/loading-spinner";
import ResultFormatter from "@/components/result-formatter";
interface Tool {
name: string;
description: string;
category?: string;
inputSchema: {
type: "object";
properties: Record<string, any>;
required?: string[];
};
}
interface ToolModalProps {
tool: Tool;
onClose: () => void;
}
export default function ToolModal({ tool, onClose }: ToolModalProps) {
const [parameters, setParameters] = useState<Record<string, any>>({});
const [showResults, setShowResults] = useState(false);
const [executionResult, setExecutionResult] = useState<any>(null);
const { toast } = useToast();
const executeToolMutation = useMutation({
mutationFn: async (params: { toolName: string; parameters: Record<string, any> }) => {
const response = await apiRequest("POST", "/api/tools/execute", params);
return response.json();
},
onSuccess: (data) => {
setExecutionResult(data.result);
setShowResults(true);
toast({
title: "Success!",
description: "Tool executed successfully.",
});
},
onError: (error: any) => {
toast({
title: "Execution Failed",
description: error.message || "Failed to execute tool. Please try again.",
variant: "destructive",
});
},
});
const handleParameterChange = (key: string, value: any) => {
setParameters(prev => ({
...prev,
[key]: value
}));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
executeToolMutation.mutate({
toolName: tool.name,
parameters
});
};
const copyResults = async () => {
if (executionResult) {
try {
await navigator.clipboard.writeText(JSON.stringify(executionResult, null, 2));
toast({
title: "Copied!",
description: "Results copied to clipboard.",
});
} catch (error) {
toast({
title: "Copy Failed",
description: "Failed to copy results to clipboard.",
variant: "destructive",
});
}
}
};
const renderParameterField = (key: string, property: any) => {
const isRequired = tool.inputSchema.required?.includes(key) || false;
const description = property.description || `${key} parameter`;
if (property.enum) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="flex items-center gap-2">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
{isRequired && <span className="text-destructive">*</span>}
</Label>
<Select onValueChange={(value) => handleParameterChange(key, value)}>
<SelectTrigger>
<SelectValue placeholder={`Select ${key}`} />
</SelectTrigger>
<SelectContent>
{property.enum.map((option: string) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
);
}
if (property.type === 'string' && (key.includes('description') || key.includes('body') || key.includes('content'))) {
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="flex items-center gap-2">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
{isRequired && <span className="text-destructive">*</span>}
</Label>
<Textarea
id={key}
placeholder={description}
value={parameters[key] || ''}
onChange={(e) => handleParameterChange(key, e.target.value)}
rows={3}
required={isRequired}
/>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
);
}
return (
<div key={key} className="space-y-2">
<Label htmlFor={key} className="flex items-center gap-2">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
{isRequired && <span className="text-destructive">*</span>}
</Label>
<Input
id={key}
type="text"
placeholder={description}
value={parameters[key] || ''}
onChange={(e) => handleParameterChange(key, e.target.value)}
required={isRequired}
/>
</div>
);
};
if (showResults) {
return (
<Dialog open={true} onOpenChange={() => setShowResults(false)}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-green-600 to-teal-600 rounded-lg flex items-center justify-center">
<CheckCircle2 className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-xl font-semibold">Execution Results</h3>
<p className="text-sm text-muted-foreground">Tool executed successfully</p>
</div>
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="max-h-[60vh] overflow-y-auto">
<ResultFormatter result={executionResult} toolName={tool.name} />
</div>
<div className="flex items-center justify-between pt-4 border-t border-border">
<div className="flex items-center text-sm text-muted-foreground">
<CheckCircle2 className="mr-2 h-4 w-4 text-green-500" />
<span>Executed successfully</span>
</div>
<div className="flex space-x-3">
<Button variant="outline" onClick={copyResults}>
<Copy className="mr-2 h-4 w-4" />
Copy Raw
</Button>
<Button onClick={() => setShowResults(false)}>
Close
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-purple-600 to-blue-600 rounded-lg flex items-center justify-center">
<Play className="h-5 w-5 text-white" />
</div>
<div>
<h3 className="text-xl font-semibold">
{tool.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</h3>
<p className="text-sm text-muted-foreground">{tool.description}</p>
</div>
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-4">
{Object.entries(tool.inputSchema.properties || {}).map(([key, property]) =>
renderParameterField(key, property)
)}
</div>
<div className="flex items-center justify-between pt-4 border-t border-border">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button
type="submit"
disabled={executeToolMutation.isPending}
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700"
>
{executeToolMutation.isPending ? (
<>
<LoadingSpinner className="mr-2 h-4 w-4" />
Executing...
</>
) : (
<>
<Play className="mr-2 h-4 w-4" />
Execute Tool
</>
)}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}