mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
14 lines
13 KiB
JSON
14 lines
13 KiB
JSON
{
|
|
"name": "ai-tools",
|
|
"description": "Tool use and function calling interfaces",
|
|
"dependencies": [
|
|
"@hanzo/ui"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "ai-tools.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Grid } from \"@hanzo/ui/grid\"\nimport {\n Calculator,\n ChevronRight,\n Code,\n FileText,\n Globe,\n Play,\n Search,\n Settings,\n Wrench,\n Zap,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n} from \"@/registry/default/ui/card\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport { Textarea } from \"@/registry/default/ui/textarea\"\n\nexport interface AITool {\n name: string\n description: string\n icon?: React.ComponentType<{ className?: string }>\n category?: string\n parameters?: Record<string, any>\n execute?: (params: any) => Promise<any>\n enabled?: boolean\n}\n\nexport interface AIToolsProps extends React.HTMLAttributes<HTMLDivElement> {\n tools?: AITool[]\n onToolExecute?: (toolName: string, params: any) => void\n onToolResult?: (toolName: string, result: any) => void\n onToolToggle?: (toolName: string, enabled: boolean) => void\n showCategories?: boolean\n searchable?: boolean\n}\n\n// Default tools\nconst defaultTools: AITool[] = [\n {\n name: \"web_search\",\n description: \"Search the web for current information\",\n icon: Search,\n category: \"Information\",\n enabled: true,\n parameters: { query: \"\" },\n },\n {\n name: \"code_execution\",\n description: \"Execute code snippets safely\",\n icon: Code,\n category: \"Development\",\n enabled: true,\n parameters: { code: \"\", language: \"javascript\" },\n },\n {\n name: \"calculator\",\n description: \"Perform mathematical calculations\",\n icon: Calculator,\n category: \"Utilities\",\n enabled: true,\n parameters: { expression: \"\" },\n },\n {\n name: \"file_reader\",\n description: \"Read and analyze file contents\",\n icon: FileText,\n category: \"Files\",\n enabled: true,\n parameters: { file_path: \"\" },\n },\n {\n name: \"api_call\",\n description: \"Make HTTP requests to external APIs\",\n icon: Globe,\n category: \"Network\",\n enabled: false,\n parameters: { url: \"\", method: \"GET\", headers: {}, body: \"\" },\n },\n]\n\nconst categoryIcons = {\n Information: Search,\n Development: Code,\n Utilities: Calculator,\n Files: FileText,\n Network: Globe,\n default: Wrench,\n}\n\nconst AITools = React.forwardRef<HTMLDivElement, AIToolsProps>(\n (\n {\n className,\n children,\n tools = defaultTools,\n onToolExecute,\n onToolResult,\n onToolToggle,\n showCategories = true,\n searchable = true,\n ...props\n },\n ref\n ) => {\n const [searchQuery, setSearchQuery] = React.useState(\"\")\n const [selectedTool, setSelectedTool] = React.useState<string | null>(null)\n const [toolParameters, setToolParameters] = React.useState<\n Record<string, any>\n >({})\n const [isExecuting, setIsExecuting] = React.useState(false)\n\n // Filter tools based on search query\n const filteredTools = React.useMemo(() => {\n if (!searchQuery) return tools\n return tools.filter(\n (tool) =>\n tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||\n tool.description.toLowerCase().includes(searchQuery.toLowerCase()) ||\n tool.category?.toLowerCase().includes(searchQuery.toLowerCase())\n )\n }, [tools, searchQuery])\n\n // Group tools by category\n const categorizedTools = React.useMemo(() => {\n const categories: Record<string, AITool[]> = {}\n filteredTools.forEach((tool) => {\n const category = tool.category || \"Other\"\n if (!categories[category]) categories[category] = []\n categories[category].push(tool)\n })\n return categories\n }, [filteredTools])\n\n const handleToolExecute = async (tool: AITool) => {\n if (!tool.enabled) return\n\n setIsExecuting(true)\n try {\n const params = toolParameters[tool.name] || tool.parameters || {}\n\n if (tool.execute) {\n const result = await tool.execute(params)\n onToolResult?.(tool.name, result)\n } else {\n onToolExecute?.(tool.name, params)\n }\n } catch (error) {\n console.error(`Tool execution failed: ${tool.name}`, error)\n } finally {\n setIsExecuting(false)\n }\n }\n\n const handleParameterChange = (\n toolName: string,\n paramName: string,\n value: any\n ) => {\n setToolParameters((prev) => ({\n ...prev,\n [toolName]: {\n ...prev[toolName],\n [paramName]: value,\n },\n }))\n }\n\n const ToolCard = ({ tool }: { tool: AITool }) => {\n const Icon = tool.icon || Wrench\n const isSelected = selectedTool === tool.name\n const params = toolParameters[tool.name] || tool.parameters || {}\n\n return (\n <Card\n className={cn(\n \"transition-all cursor-pointer\",\n isSelected && \"ring-2 ring-primary\",\n !tool.enabled && \"opacity-50\"\n )}\n onClick={() => setSelectedTool(isSelected ? null : tool.name)}\n >\n <CardHeader className=\"pb-2\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <div\n className={cn(\n \"flex h-8 w-8 items-center justify-center rounded-lg\",\n tool.enabled\n ? \"bg-primary/10 text-primary\"\n : \"bg-muted text-muted-foreground\"\n )}\n >\n <Icon className=\"h-4 w-4\" />\n </div>\n <div>\n <CardTitle className=\"text-sm font-medium\">\n {tool.name}\n </CardTitle>\n {tool.category && (\n <Badge variant=\"outline\" className=\"text-xs mt-1\">\n {tool.category}\n </Badge>\n )}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {onToolToggle && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(e) => {\n e.stopPropagation()\n onToolToggle(tool.name, !tool.enabled)\n }}\n >\n <Settings className=\"h-3 w-3\" />\n </Button>\n )}\n <ChevronRight\n className={cn(\n \"h-4 w-4 transition-transform\",\n isSelected && \"rotate-90\"\n )}\n />\n </div>\n </div>\n </CardHeader>\n <CardContent className=\"pt-0\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {tool.description}\n </p>\n\n {isSelected && Object.keys(params).length > 0 && (\n <Grid columns={1} gap={12} className=\"border-t pt-3\">\n <h4 className=\"text-sm font-medium\">Parameters</h4>\n {/* One field per parameter, two-up as soon as the card is wide\n enough to hold two 220px fields. */}\n <Grid columns={{ min: 220, max: 2 }} gap={12}>\n {Object.entries(params).map(([paramName, paramValue]) => (\n <Grid key={paramName} columns={1} gap={4}>\n <label className=\"text-xs font-medium capitalize\">\n {paramName.replace(/_/g, \" \")}\n </label>\n {typeof paramValue === \"string\" &&\n paramValue.length > 50 ? (\n <Textarea\n value={params[paramName] || \"\"}\n onChange={(e) =>\n handleParameterChange(\n tool.name,\n paramName,\n e.target.value\n )\n }\n placeholder={`Enter ${paramName}`}\n className=\"text-xs\"\n rows={3}\n />\n ) : (\n <Input\n value={params[paramName] || \"\"}\n onChange={(e) =>\n handleParameterChange(\n tool.name,\n paramName,\n e.target.value\n )\n }\n placeholder={`Enter ${paramName}`}\n className=\"text-xs\"\n />\n )}\n </Grid>\n ))}\n </Grid>\n <Button\n onClick={(e) => {\n e.stopPropagation()\n handleToolExecute(tool)\n }}\n disabled={!tool.enabled || isExecuting}\n size=\"sm\"\n className=\"w-full\"\n >\n {isExecuting ? (\n <>\n <Zap className=\"h-3 w-3 mr-2 animate-spin\" />\n Executing...\n </>\n ) : (\n <>\n <Play className=\"h-3 w-3 mr-2\" />\n Execute\n </>\n )}\n </Button>\n </Grid>\n )}\n </CardContent>\n </Card>\n )\n }\n\n return (\n <Grid ref={ref} columns={1} gap={16} className={className} {...props}>\n {/* Header */}\n <div className=\"flex items-center gap-2\">\n <Wrench className=\"h-5 w-5\" />\n <h3 className=\"font-semibold\">AI Tools</h3>\n <Badge variant=\"secondary\">\n {tools.filter((t) => t.enabled).length} enabled\n </Badge>\n </div>\n\n {/* Search */}\n {searchable && (\n <Input\n placeholder=\"Search tools...\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full\"\n />\n )}\n\n {/* Tools */}\n <ScrollArea className=\"h-[400px]\">\n <Grid columns={1} gap={16}>\n {showCategories\n ? Object.entries(categorizedTools).map(\n ([category, categoryTools]) => {\n const CategoryIcon =\n categoryIcons[category as keyof typeof categoryIcons] ||\n categoryIcons.default\n return (\n <Grid key={category} columns={1} gap={8}>\n <div className=\"flex items-center gap-2\">\n <CategoryIcon className=\"h-4 w-4 text-muted-foreground\" />\n <h4 className=\"font-medium text-sm\">{category}</h4>\n <Separator className=\"flex-1\" />\n </div>\n <Grid columns={1} gap={8} className=\"pl-6\">\n {categoryTools.map((tool) => (\n <ToolCard key={tool.name} tool={tool} />\n ))}\n </Grid>\n </Grid>\n )\n }\n )\n : filteredTools.map((tool) => (\n <ToolCard key={tool.name} tool={tool} />\n ))}\n\n {filteredTools.length === 0 && (\n <Grid\n columns={1}\n gap={4}\n className=\"py-8 text-center\"\n style={{ justifyItems: \"center\" }}\n >\n <Wrench className=\"h-12 w-12 text-muted-foreground/50\" />\n <h4 className=\"font-medium text-muted-foreground\">\n No tools found\n </h4>\n <p className=\"text-sm text-muted-foreground\">\n Try adjusting your search criteria\n </p>\n </Grid>\n )}\n </Grid>\n </ScrollArea>\n\n {children}\n </Grid>\n )\n }\n)\nAITools.displayName = \"AITools\"\n\nexport { AITools }\n"
|
|
}
|
|
],
|
|
"type": "components:ai"
|
|
} |