feat(playground): enhance tool list UI with package names and detail modal

Add two major UX improvements to the ToolsSidebar:

1. Package Name Display
   - Show package name as secondary label below tool name
   - Improves tool identification at a glance

2. Tool Detail Modal
   - Click any tool card to open detailed modal
   - Shows comprehensive information:
     * Tool name, package, version, category
     * Quality score (if available)
     * Full description
     * Supported frameworks (badges)
     * Environment variables (with required flag)
     * Import URL (for manual integration)
     * Tool ID (for debugging)
   - Modal features:
     * Backdrop blur effect
     * Click outside or press Escape to close
     * Close button (X icon) with SVG title
     * Responsive layout (max-w-2xl)
     * Scrollable content (max-h-90vh)
     * Full keyboard accessibility (ARIA labels, tabIndex, Escape key)

3. Enhanced Tool Interface
   - Added optional fields: toolId, qualityScore, frameworks, env, importUrl
   - Maintains backward compatibility with existing API

This gives users full visibility into tool metadata and helps them
understand what each tool does before using it in conversations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-12-04 18:20:33 +10:00
parent 56a6128ac6
commit addb6ba06e

View file

@ -6,17 +6,23 @@ import { Input } from '@tpmjs/ui/Input/Input';
import { useEffect, useState } from 'react';
interface Tool {
toolId?: string;
packageName: string;
exportName: string;
description: string;
category: string;
version: string;
qualityScore?: number;
frameworks?: string[];
env?: Record<string, { description: string; required?: boolean }>;
importUrl?: string;
}
export function ToolsSidebar(): React.ReactElement {
const [tools, setTools] = useState<Tool[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('');
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
useEffect(() => {
async function fetchTools() {
@ -44,49 +50,178 @@ export function ToolsSidebar(): React.ReactElement {
);
return (
<aside className="hidden w-64 border-r border-border bg-surface md:block">
<div className="flex h-full flex-col p-4">
<h2 className="mb-4 text-lg font-bold">
Available Tools <Badge variant="secondary">{filteredTools.length}</Badge>
</h2>
<>
<aside className="hidden w-64 border-r border-border bg-surface md:block">
<div className="flex h-full flex-col p-4">
<h2 className="mb-4 text-lg font-bold">
Available Tools <Badge variant="secondary">{filteredTools.length}</Badge>
</h2>
<Input
type="text"
placeholder="Filter tools..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="mb-4"
/>
<Input
type="text"
placeholder="Filter tools..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="mb-4"
/>
<div className="flex-1 space-y-2 overflow-y-auto">
{loading ? (
<p className="text-sm text-foreground-secondary">Loading tools...</p>
) : filteredTools.length === 0 ? (
<p className="text-sm text-foreground-secondary">No tools found</p>
) : (
filteredTools.map((tool) => (
<Card
key={`${tool.packageName}-${tool.exportName}`}
variant="outline"
className="cursor-pointer hover:bg-background"
>
<CardHeader>
<CardTitle className="text-sm">{tool.exportName}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-xs text-foreground-secondary">{tool.description}</p>
<div className="mt-2 flex items-center gap-2">
<Badge variant="secondary" size="sm">
{tool.category}
</Badge>
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
</div>
</CardContent>
</Card>
))
)}
<div className="flex-1 space-y-2 overflow-y-auto">
{loading ? (
<p className="text-sm text-foreground-secondary">Loading tools...</p>
) : filteredTools.length === 0 ? (
<p className="text-sm text-foreground-secondary">No tools found</p>
) : (
filteredTools.map((tool) => (
<Card
key={`${tool.packageName}-${tool.exportName}`}
variant="outline"
className="cursor-pointer transition-all hover:bg-background hover:shadow-md"
onClick={() => setSelectedTool(tool)}
>
<CardHeader>
<CardTitle className="text-sm">{tool.exportName}</CardTitle>
<p className="text-xs text-foreground-tertiary mt-0.5">{tool.packageName}</p>
</CardHeader>
<CardContent>
<p className="text-xs text-foreground-secondary line-clamp-2">
{tool.description}
</p>
<div className="mt-2 flex items-center gap-2">
<Badge variant="secondary" size="sm">
{tool.category}
</Badge>
<span className="text-xs text-foreground-tertiary">v{tool.version}</span>
</div>
</CardContent>
</Card>
))
)}
</div>
</div>
</div>
</aside>
</aside>
{/* Tool Details Modal */}
{selectedTool && (
{/* biome-ignore lint/a11y/useSemanticElements: Modal backdrop - standard pattern for modal overlays with role=button */}
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={() => setSelectedTool(null)}
onKeyDown={(e) => {
if (e.key === 'Escape') setSelectedTool(null);
}}
role="button"
tabIndex={0}
aria-label="Close modal"
>
{/* biome-ignore lint/a11y/useKeyWithClickEvents: Modal content - prevents click-through to backdrop */}
<div
className="relative mx-4 max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-surface p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={() => setSelectedTool(null)}
className="absolute right-4 top-4 text-foreground-tertiary hover:text-foreground"
aria-label="Close modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<title>Close</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/* Tool header */}
<div className="mb-6 border-b border-border pb-4">
<h2 className="mb-2 text-2xl font-bold text-foreground">{selectedTool.exportName}</h2>
<p className="mb-2 text-sm text-foreground-secondary">{selectedTool.packageName}</p>
<div className="flex items-center gap-2">
<Badge variant="secondary">{selectedTool.category}</Badge>
<span className="text-sm text-foreground-tertiary">v{selectedTool.version}</span>
{selectedTool.qualityScore && (
<Badge variant="info">
Score: {(selectedTool.qualityScore * 100).toFixed(0)}%
</Badge>
)}
</div>
</div>
{/* Description */}
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Description</h3>
<p className="text-sm text-foreground-secondary">{selectedTool.description}</p>
</div>
{/* Frameworks */}
{selectedTool.frameworks && selectedTool.frameworks.length > 0 && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Frameworks</h3>
<div className="flex flex-wrap gap-2">
{selectedTool.frameworks.map((framework) => (
<Badge key={framework} variant="outline" size="sm">
{framework}
</Badge>
))}
</div>
</div>
)}
{/* Environment Variables */}
{selectedTool.env && Object.keys(selectedTool.env).length > 0 && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">
Environment Variables
</h3>
<div className="space-y-2">
{Object.entries(selectedTool.env).map(([key, config]) => (
<div key={key} className="rounded border border-border bg-background p-3">
<div className="mb-1 flex items-center gap-2">
<code className="text-sm font-mono text-foreground">{key}</code>
{config.required && (
<Badge variant="error" size="sm">
Required
</Badge>
)}
</div>
<p className="text-xs text-foreground-secondary">{config.description}</p>
</div>
))}
</div>
</div>
)}
{/* Import URL */}
{selectedTool.importUrl && (
<div className="mb-6">
<h3 className="mb-2 text-lg font-semibold text-foreground">Import URL</h3>
<code className="block rounded bg-background p-3 text-xs font-mono text-foreground-secondary break-all">
{selectedTool.importUrl}
</code>
</div>
)}
{/* Tool ID */}
{selectedTool.toolId && (
<div>
<h3 className="mb-2 text-lg font-semibold text-foreground">Tool ID</h3>
<code className="block rounded bg-background p-3 text-xs font-mono text-foreground-secondary">
{selectedTool.toolId}
</code>
</div>
)}
</div>
</div>
)}
</>
);
}