feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
341
dexto/packages/webui/components/Playground/ToolInputForm.tsx
Normal file
341
dexto/packages/webui/components/Playground/ToolInputForm.tsx
Normal file
@@ -0,0 +1,341 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Copy, Share2, Zap } from 'lucide-react';
|
||||
import type { McpTool } from '@/components/hooks/useServers';
|
||||
|
||||
// Infer the property schema type from the tool's input schema
|
||||
type JsonSchemaProperty = NonNullable<NonNullable<McpTool['inputSchema']>['properties']>[string];
|
||||
|
||||
interface ToolInputFormProps {
|
||||
tool: McpTool;
|
||||
inputs: Record<string, any>;
|
||||
errors: Record<string, string>;
|
||||
isLoading: boolean;
|
||||
onInputChange: (
|
||||
name: string,
|
||||
value: any,
|
||||
type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array'
|
||||
) => void;
|
||||
onSubmit: () => void;
|
||||
onCopyConfig?: () => void;
|
||||
onShareConfig?: () => void;
|
||||
}
|
||||
|
||||
interface ToolTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
apply: (tool: McpTool) => Record<string, any>;
|
||||
}
|
||||
|
||||
const toolTemplates: ToolTemplate[] = [
|
||||
{
|
||||
name: 'Quick Test',
|
||||
description: 'Fill with test values',
|
||||
apply: (tool: McpTool) => {
|
||||
const defaults: Record<string, any> = {};
|
||||
if (tool.inputSchema?.properties) {
|
||||
Object.entries(tool.inputSchema.properties).forEach(
|
||||
([key, prop]: [string, any]) => {
|
||||
if (prop.type === 'string') defaults[key] = `test-${key}`;
|
||||
else if (prop.type === 'number') defaults[key] = 42;
|
||||
else if (prop.type === 'boolean') defaults[key] = true;
|
||||
else if (prop.type === 'object') defaults[key] = '{"example": "value"}';
|
||||
else if (prop.type === 'array') defaults[key] = '["example"]';
|
||||
}
|
||||
);
|
||||
}
|
||||
return defaults;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Required Only',
|
||||
description: 'Fill only required fields',
|
||||
apply: (tool: McpTool) => {
|
||||
const defaults: Record<string, any> = {};
|
||||
if (tool.inputSchema?.properties && tool.inputSchema?.required) {
|
||||
tool.inputSchema.required.forEach((key: string) => {
|
||||
const prop = tool.inputSchema!.properties![key];
|
||||
if (prop.type === 'string') defaults[key] = '';
|
||||
else if (prop.type === 'number') defaults[key] = '';
|
||||
else if (prop.type === 'boolean') defaults[key] = false;
|
||||
else if (prop.type === 'object') defaults[key] = '{}';
|
||||
else if (prop.type === 'array') defaults[key] = '[]';
|
||||
});
|
||||
}
|
||||
return defaults;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Clear All',
|
||||
description: 'Clear all fields',
|
||||
apply: () => ({}),
|
||||
},
|
||||
];
|
||||
|
||||
export function ToolInputForm({
|
||||
tool,
|
||||
inputs,
|
||||
errors,
|
||||
isLoading,
|
||||
onInputChange,
|
||||
onSubmit,
|
||||
onCopyConfig,
|
||||
onShareConfig,
|
||||
}: ToolInputFormProps) {
|
||||
const hasInputs =
|
||||
tool.inputSchema?.properties && Object.keys(tool.inputSchema.properties).length > 0;
|
||||
|
||||
const renderInput = (key: string, prop: JsonSchemaProperty) => {
|
||||
const isRequired = tool.inputSchema?.required?.includes(key);
|
||||
const errorMsg = errors[key];
|
||||
const baseInputClassName = `w-full ${errorMsg ? 'border-destructive focus-visible:ring-destructive' : ''}`;
|
||||
|
||||
// Enum select
|
||||
if (prop.enum && Array.isArray(prop.enum)) {
|
||||
const isEnumBoolean = prop.enum.every(
|
||||
(v: string | number | boolean) => typeof v === 'boolean'
|
||||
);
|
||||
const isEnumNumeric = prop.enum.every(
|
||||
(v: string | number | boolean) => typeof v === 'number'
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
value={
|
||||
inputs[key] === undefined && prop.default !== undefined
|
||||
? String(prop.default)
|
||||
: String(inputs[key] || '')
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
let parsedValue: string | number | boolean = value;
|
||||
if (isEnumBoolean) parsedValue = value === 'true';
|
||||
else if (isEnumNumeric) parsedValue = Number(value);
|
||||
onInputChange(key, parsedValue, prop.type);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger id={key} className={baseInputClassName}>
|
||||
<SelectValue
|
||||
placeholder={`Select ${prop.description || key}${isRequired ? '' : ' (optional)'}...`}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{prop.enum.map((enumValue: string | number | boolean) => (
|
||||
<SelectItem key={String(enumValue)} value={String(enumValue)}>
|
||||
{String(enumValue)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
// Boolean checkbox
|
||||
if (prop.type === 'boolean') {
|
||||
return (
|
||||
<Checkbox
|
||||
id={key}
|
||||
checked={
|
||||
inputs[key] === undefined && prop.default !== undefined
|
||||
? Boolean(prop.default)
|
||||
: Boolean(inputs[key])
|
||||
}
|
||||
onCheckedChange={(checked) => onInputChange(key, checked, prop.type)}
|
||||
disabled={isLoading}
|
||||
className={errorMsg ? 'border-destructive ring-destructive' : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Object/Array textarea
|
||||
if (prop.type === 'object' || prop.type === 'array') {
|
||||
return (
|
||||
<Textarea
|
||||
id={key}
|
||||
value={
|
||||
inputs[key] === undefined && prop.default !== undefined
|
||||
? JSON.stringify(prop.default, null, 2)
|
||||
: inputs[key] || ''
|
||||
}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onInputChange(key, e.target.value, prop.type)
|
||||
}
|
||||
rows={5}
|
||||
className={`${baseInputClassName} font-mono text-sm min-h-[100px] resize-y`}
|
||||
placeholder={`Enter JSON for ${prop.description || key}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// String/Number input
|
||||
let inputFieldType: React.HTMLInputTypeAttribute = 'text';
|
||||
if (prop.type === 'number' || prop.type === 'integer') inputFieldType = 'number';
|
||||
if (prop.format === 'date-time') inputFieldType = 'datetime-local';
|
||||
if (prop.format === 'date') inputFieldType = 'date';
|
||||
if (prop.format === 'email') inputFieldType = 'email';
|
||||
if (prop.format === 'uri') inputFieldType = 'url';
|
||||
if (prop.format === 'password') inputFieldType = 'password';
|
||||
|
||||
return (
|
||||
<Input
|
||||
type={inputFieldType}
|
||||
id={key}
|
||||
value={
|
||||
inputs[key] === undefined && prop.default !== undefined
|
||||
? String(prop.default)
|
||||
: String(inputs[key] || '')
|
||||
}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
onInputChange(key, e.target.value, prop.type)
|
||||
}
|
||||
className={baseInputClassName}
|
||||
placeholder={prop.description || `Enter ${key}`}
|
||||
disabled={isLoading}
|
||||
step={prop.type === 'number' || prop.type === 'integer' ? 'any' : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
className="space-y-5 p-4 border border-border rounded-lg bg-card shadow-sm"
|
||||
>
|
||||
{/* Quick Fill Templates */}
|
||||
{hasInputs && (
|
||||
<div className="border-b border-border pb-4">
|
||||
<h4 className="text-sm font-medium mb-2 text-muted-foreground">Quick Fill</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{toolTemplates.map((template, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newInputs = template.apply(tool);
|
||||
Object.entries(newInputs).forEach(([key, value]) => {
|
||||
const prop = tool.inputSchema?.properties?.[key];
|
||||
onInputChange(key, value, prop?.type);
|
||||
});
|
||||
}}
|
||||
className="text-xs"
|
||||
title={template.description}
|
||||
>
|
||||
{template.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form Inputs */}
|
||||
{!hasInputs && (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
This tool does not require any inputs.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasInputs &&
|
||||
Object.entries(tool.inputSchema!.properties!).map(([key, prop]) => {
|
||||
const isRequired = tool.inputSchema?.required?.includes(key);
|
||||
const errorMsg = errors[key];
|
||||
|
||||
return (
|
||||
<div key={key} className="grid gap-1.5">
|
||||
<div
|
||||
className={`flex ${
|
||||
prop.type === 'boolean'
|
||||
? 'flex-row items-center space-x-3'
|
||||
: 'flex-col'
|
||||
}`}
|
||||
>
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className={`${
|
||||
prop.type === 'boolean'
|
||||
? 'leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
: 'capitalize font-medium'
|
||||
}`}
|
||||
>
|
||||
{prop.description ||
|
||||
key
|
||||
.replace(/([A-Z]+(?=[A-Z][a-z]))|([A-Z][a-z])/g, ' $&')
|
||||
.trim()
|
||||
.replace(/_/g, ' ')}
|
||||
{isRequired && (
|
||||
<span className="text-destructive text-lg ml-0.5">*</span>
|
||||
)}
|
||||
</Label>
|
||||
{prop.type === 'boolean' ? (
|
||||
renderInput(key, prop)
|
||||
) : (
|
||||
<div className="w-full">{renderInput(key, prop)}</div>
|
||||
)}
|
||||
</div>
|
||||
{errorMsg && <p className="text-xs text-destructive">{errorMsg}</p>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || Object.keys(errors).some((k) => errors[k] !== '')}
|
||||
className="flex-1"
|
||||
>
|
||||
{isLoading ? (
|
||||
'Executing...'
|
||||
) : (
|
||||
<>
|
||||
<Zap className="h-4 w-4 mr-2" />
|
||||
Run Tool
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{hasInputs && Object.keys(inputs).length > 0 && (
|
||||
<>
|
||||
{onCopyConfig && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCopyConfig}
|
||||
>
|
||||
<Copy className="h-3 w-3 mr-2" />
|
||||
Copy
|
||||
</Button>
|
||||
)}
|
||||
{onShareConfig && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onShareConfig}
|
||||
>
|
||||
<Share2 className="h-3 w-3 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user