feat: add reusable ToolHealthBadge and ToolHealthBanner components

- Create ToolHealthBadge component in @tpmjs/ui for broken tool indicator
- Create ToolHealthBanner component in @tpmjs/ui for detailed health warnings
- Integrate both components into playground ToolsSidebar:
  - Badge shows in tool cards in left sidebar
  - Banner shows in tool detail modal
- Update search-registry to include health fields in API responses
- Add package.json exports for new health components

These components provide consistent UI for displaying broken tool status
across the application (tool search, tool detail pages, playground).

🤖 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 19:12:07 +10:00
parent 54ee6439cb
commit a9e91c02d1
24 changed files with 729 additions and 405 deletions

View file

@ -1,5 +1,7 @@
import { cn } from '@tpmjs/utils/cn';
import { forwardRef, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Icon } from '../Icon/Icon';
import type { CodeBlockProps } from './types';
import {
@ -8,11 +10,75 @@ import {
codeBlockCopyButtonVariants,
} from './variants';
// Custom light theme with better contrast
const customLightTheme = {
'code[class*="language-"]': {
color: '#24292e',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: '4',
hyphens: 'none',
},
'pre[class*="language-"]': {
color: '#24292e',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
fontSize: '1em',
textAlign: 'left',
whiteSpace: 'pre',
wordSpacing: 'normal',
wordBreak: 'normal',
wordWrap: 'normal',
lineHeight: '1.5',
tabSize: '4',
hyphens: 'none',
padding: '1em',
margin: '0.5em 0',
overflow: 'auto',
},
comment: { color: '#6a737d', fontStyle: 'italic' },
prolog: { color: '#6a737d' },
doctype: { color: '#6a737d' },
cdata: { color: '#6a737d' },
punctuation: { color: '#24292e' },
property: { color: '#005cc5' },
tag: { color: '#22863a' },
boolean: { color: '#005cc5' },
number: { color: '#005cc5' },
constant: { color: '#005cc5' },
symbol: { color: '#005cc5' },
deleted: { color: '#b31d28' },
selector: { color: '#22863a' },
'attr-name': { color: '#6f42c1' },
string: { color: '#032f62' },
char: { color: '#032f62' },
builtin: { color: '#005cc5' },
inserted: { color: '#22863a' },
operator: { color: '#d73a49' },
entity: { color: '#6f42c1' },
url: { color: '#032f62' },
variable: { color: '#e36209' },
atrule: { color: '#d73a49' },
'attr-value': { color: '#032f62' },
function: { color: '#6f42c1' },
'class-name': { color: '#6f42c1' },
keyword: { color: '#d73a49' },
regex: { color: '#032f62' },
important: { color: '#d73a49', fontWeight: 'bold' },
bold: { fontWeight: 'bold' },
italic: { fontStyle: 'italic' },
};
/**
* CodeBlock component
*
* Displays formatted code with optional copy functionality.
* Includes syntax-highlighted display and copy-to-clipboard button.
* Displays formatted code with syntax highlighting and optional copy functionality.
* Uses Prism syntax highlighter for beautiful code display.
* Built with React and JSX.
*
* @example
@ -32,7 +98,10 @@ import {
* ```
*/
export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
({ className, code, language = 'text', size = 'md', showCopy = true, ...props }, ref) => {
(
{ className, code, language = 'text', size = 'md', showCopy = true, theme = 'light', ...props },
ref
) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
@ -46,16 +115,42 @@ export const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(
}
};
// Map language aliases to supported languages
const languageMap: Record<string, string> = {
js: 'javascript',
ts: 'typescript',
jsx: 'jsx',
tsx: 'tsx',
py: 'python',
rb: 'ruby',
sh: 'bash',
yml: 'yaml',
};
const normalizedLanguage = languageMap[language] || language;
return (
<div ref={ref} className={cn(codeBlockContainerVariants(), className)} {...props}>
<code
className={codeBlockCodeVariants({
size,
})}
data-language={language}
>
{code}
</code>
<div className={codeBlockCodeVariants({ size })} data-language={normalizedLanguage}>
<SyntaxHighlighter
language={normalizedLanguage}
style={theme === 'dark' ? oneDark : oneLight}
customStyle={{
margin: 0,
padding: 0,
background: 'transparent',
fontSize: 'inherit',
lineHeight: 'inherit',
}}
codeTagProps={{
style: {
fontFamily: 'inherit',
},
}}
>
{code}
</SyntaxHighlighter>
</div>
{showCopy && (
<button
type="button"

View file

@ -10,7 +10,7 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
code: string;
/**
* Programming language (for display/metadata only)
* Programming language for syntax highlighting
* @default 'text'
*/
language?: string;
@ -26,6 +26,12 @@ export interface CodeBlockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
* @default true
*/
showCopy?: boolean;
/**
* Color theme for syntax highlighting
* @default 'light'
*/
theme?: 'light' | 'dark';
}
/**

View file

@ -12,8 +12,8 @@ export const inputVariants = createVariants({
'font-sans',
// Borders & Radius
'rounded-md border',
// Background
'bg-background',
// Background - Pure white to stand out
'bg-surface',
// Transitions
'transition-base',
// Focus

View file

@ -12,8 +12,8 @@ export const textareaVariants = createVariants({
'font-sans',
// Borders & Radius
'rounded-md border',
// Background
'bg-background',
// Background - Pure white to stand out
'bg-surface',
// Transitions
'transition-base',
// Focus

View file

@ -0,0 +1,35 @@
'use client';
import type React from 'react';
import { Badge } from '../Badge/Badge';
import { Icon } from '../Icon/Icon';
export interface ToolHealthBadgeProps {
importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
size?: 'sm' | 'md' | 'lg';
className?: string;
}
/**
* Badge component to indicate if a tool is broken (import or execution failed)
*/
export function ToolHealthBadge({
importHealth,
executionHealth,
size = 'sm',
className,
}: ToolHealthBadgeProps): React.ReactElement | null {
const isBroken = importHealth === 'BROKEN' || executionHealth === 'BROKEN';
if (!isBroken) {
return null;
}
return (
<Badge variant="error" size={size} className={className}>
<Icon icon="x" size="sm" className="mr-1" />
Broken
</Badge>
);
}

View file

@ -0,0 +1,86 @@
'use client';
import type React from 'react';
import { Badge } from '../Badge/Badge';
export interface ToolHealthBannerProps {
importHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
executionHealth?: 'HEALTHY' | 'BROKEN' | 'UNKNOWN';
healthCheckError?: string | null;
lastHealthCheck?: string | null;
onRecheck?: () => void;
recheckLoading?: boolean;
className?: string;
}
/**
* Banner component to display detailed health status for broken tools
*/
export function ToolHealthBanner({
importHealth,
executionHealth,
healthCheckError,
lastHealthCheck,
onRecheck,
recheckLoading = false,
className,
}: ToolHealthBannerProps): React.ReactElement | null {
const isBroken = importHealth === 'BROKEN' || executionHealth === 'BROKEN';
if (!isBroken) {
return null;
}
return (
<div
className={`p-4 rounded-lg bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 ${className || ''}`}
>
<div className="flex items-start gap-3">
<span className="text-xl mt-0.5"></span>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 dark:text-red-300 mb-1">
This tool is currently broken
</h3>
<div className="space-y-1 text-sm text-red-700 dark:text-red-400">
{importHealth === 'BROKEN' && (
<div className="flex items-center gap-2">
<Badge variant="error" size="sm">
Import Failed
</Badge>
<span className="text-xs">Cannot load from Railway service</span>
</div>
)}
{executionHealth === 'BROKEN' && (
<div className="flex items-center gap-2">
<Badge variant="error" size="sm">
Execution Failed
</Badge>
<span className="text-xs">Runtime error with test parameters</span>
</div>
)}
</div>
{healthCheckError && (
<pre className="mt-2 p-2 rounded bg-red-100 dark:bg-red-900/30 text-xs font-mono text-red-800 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
{healthCheckError}
</pre>
)}
{lastHealthCheck && (
<p className="text-xs text-red-600 dark:text-red-500 mt-2">
Last checked: {new Date(lastHealthCheck).toLocaleString()}
</p>
)}
{onRecheck && (
<button
type="button"
onClick={onRecheck}
disabled={recheckLoading}
className="mt-3 text-sm font-medium text-red-700 dark:text-red-400 hover:underline disabled:opacity-50"
>
{recheckLoading ? 'Rechecking...' : 'Recheck health →'}
</button>
)}
</div>
</div>
</div>
);
}

View file

@ -14,8 +14,8 @@ export const formInputBase = [
'font-sans text-base',
// Borders and radius
'rounded-md border border-border',
// Colors
'bg-background text-foreground',
// Colors - Pure white background to stand out
'bg-surface text-foreground',
// Placeholder
'placeholder:text-foreground-tertiary',
// Transitions