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:
parent
54ee6439cb
commit
a9e91c02d1
24 changed files with 729 additions and 405 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35
packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx
Normal file
35
packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
86
packages/ui/src/ToolHealthBanner/ToolHealthBanner.tsx
Normal file
86
packages/ui/src/ToolHealthBanner/ToolHealthBanner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue