See the React Native example for a complete implementation with a dynamic tool UI registry pattern.
Architecture
Instead of displaying all tool results the same way, we render different components based on thedisplayType:
function MessageRenderer({ entry }: { entry: ProcessedEntry }) {
switch (entry.displayType) {
case 'generative_image':
return <ImageGenerationUI entry={entry} />;
case 'generative_video':
return <VideoGenerationUI entry={entry} />;
case 'generative_audio':
return <AudioGenerationUI entry={entry} />;
case 'background_removal':
return <ImageProcessingUI entry={entry} />;
case 'upscale':
return <UpscaleUI entry={entry} />;
default:
return <DefaultToolUI entry={entry} />;
}
}
Image Generation UI
import { useState } from 'react';
import type { ProcessedEntry } from '@gr33n-ai/jade-sdk-client';
function ImageGenerationUI({ entry }: { entry: ProcessedEntry }) {
const [isExpanded, setIsExpanded] = useState(false);
const prompt = entry.parsedInput?.data?.prompt as string;
const aspectRatio = entry.parsedInput?.data?.aspect_ratio as string;
const model = entry.parsedInput?.data?.model as string;
const imageUrl = entry.mediaUrls?.[0];
return (
<div style={styles.card}>
{/* Header */}
<div style={styles.header}>
<span style={styles.icon}>🎨</span>
<span style={styles.title}>Image Generation</span>
{model && <span style={styles.badge}>{model}</span>}
</div>
{/* Prompt */}
<div style={styles.promptSection}>
<p style={styles.prompt}>{prompt}</p>
{aspectRatio && (
<span style={styles.aspectBadge}>{aspectRatio}</span>
)}
</div>
{/* Image */}
{imageUrl && (
<div style={styles.imageContainer}>
<img
src={imageUrl}
alt={prompt}
style={{
...styles.image,
...(isExpanded ? styles.imageExpanded : {}),
}}
onClick={() => setIsExpanded(!isExpanded)}
/>
</div>
)}
{/* Actions */}
<div style={styles.actions}>
<button onClick={() => downloadImage(imageUrl!)} style={styles.actionButton}>
Download
</button>
<button onClick={() => copyPrompt(prompt)} style={styles.actionButton}>
Copy Prompt
</button>
<button onClick={() => regenerate(prompt)} style={styles.actionButton}>
Regenerate
</button>
</div>
</div>
);
}
const styles: Record<string, React.CSSProperties> = {
card: {
border: '1px solid #e5e7eb',
borderRadius: 12,
overflow: 'hidden',
marginBottom: 16,
},
header: {
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '12px 16px',
backgroundColor: '#f9fafb',
borderBottom: '1px solid #e5e7eb',
},
icon: { fontSize: 20 },
title: { fontWeight: 600, flex: 1 },
badge: {
fontSize: 12,
padding: '2px 8px',
backgroundColor: '#e0e7ff',
color: '#4f46e5',
borderRadius: 12,
},
promptSection: {
padding: 16,
display: 'flex',
alignItems: 'flex-start',
gap: 12,
},
prompt: {
flex: 1,
margin: 0,
color: '#374151',
lineHeight: 1.5,
},
aspectBadge: {
fontSize: 12,
padding: '4px 8px',
backgroundColor: '#f3f4f6',
borderRadius: 4,
},
imageContainer: {
padding: '0 16px 16px',
},
image: {
width: '100%',
borderRadius: 8,
cursor: 'pointer',
transition: 'transform 0.2s',
},
imageExpanded: {
transform: 'scale(1.05)',
},
actions: {
display: 'flex',
gap: 8,
padding: '12px 16px',
borderTop: '1px solid #e5e7eb',
backgroundColor: '#f9fafb',
},
actionButton: {
padding: '8px 16px',
border: '1px solid #d1d5db',
borderRadius: 6,
backgroundColor: 'white',
cursor: 'pointer',
fontSize: 14,
},
};
Video Generation UI
function VideoGenerationUI({ entry }: { entry: ProcessedEntry }) {
const [isPlaying, setIsPlaying] = useState(false);
const prompt = entry.parsedInput?.data?.prompt as string;
const duration = entry.parsedInput?.data?.duration as number;
const imageUrl = entry.parsedInput?.data?.image_url as string;
const videoUrl = entry.mediaUrls?.[0];
return (
<div style={styles.card}>
{/* Header */}
<div style={styles.header}>
<span style={styles.icon}>🎬</span>
<span style={styles.title}>Video Generation</span>
{duration && <span style={styles.badge}>{duration}s</span>}
</div>
{/* Source info */}
{imageUrl && (
<div style={styles.sourceSection}>
<span style={styles.sourceLabel}>Starting from:</span>
<img src={imageUrl} alt="Source" style={styles.sourceThumb} />
</div>
)}
{/* Prompt */}
<div style={styles.promptSection}>
<p style={styles.prompt}>{prompt}</p>
</div>
{/* Video */}
{videoUrl && (
<div style={styles.videoContainer}>
<video
src={videoUrl}
controls
style={styles.video}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
/>
</div>
)}
{/* Status indicator */}
{isPlaying && (
<div style={styles.playingIndicator}>
<span>▶ Playing</span>
</div>
)}
{/* Actions */}
<div style={styles.actions}>
<button onClick={() => downloadVideo(videoUrl!)} style={styles.actionButton}>
Download MP4
</button>
<button onClick={() => shareVideo(videoUrl!)} style={styles.actionButton}>
Share
</button>
</div>
</div>
);
}
Audio Generation UI
function AudioGenerationUI({ entry }: { entry: ProcessedEntry }) {
const mode = entry.parsedInput?.data?.mode as string;
const prompt = entry.parsedInput?.data?.prompt as string;
const text = entry.parsedInput?.data?.text as string;
const audioUrl = entry.mediaUrls?.[0];
// Different icons/titles based on mode
const modeConfig = {
narration: { icon: '🎙️', title: 'Narration' },
sound_effects: { icon: '🔊', title: 'Sound Effect' },
music: { icon: '🎵', title: 'Music Generation' },
}[mode] || { icon: '🔈', title: 'Audio' };
return (
<div style={styles.card}>
{/* Header */}
<div style={styles.header}>
<span style={styles.icon}>{modeConfig.icon}</span>
<span style={styles.title}>{modeConfig.title}</span>
</div>
{/* Content based on mode */}
<div style={styles.promptSection}>
{mode === 'narration' ? (
<blockquote style={styles.narrationText}>"{text}"</blockquote>
) : (
<p style={styles.prompt}>{prompt}</p>
)}
</div>
{/* Waveform visualization */}
{audioUrl && (
<div style={styles.audioContainer}>
<div style={styles.waveform}>
{/* Animated waveform bars */}
{[...Array(20)].map((_, i) => (
<div
key={i}
style={{
...styles.waveformBar,
animationDelay: `${i * 0.05}s`,
}}
/>
))}
</div>
<audio src={audioUrl} controls style={styles.audioPlayer} />
</div>
)}
{/* Actions */}
<div style={styles.actions}>
<button onClick={() => downloadAudio(audioUrl!)} style={styles.actionButton}>
Download
</button>
{mode === 'narration' && (
<button onClick={() => copyText(text)} style={styles.actionButton}>
Copy Text
</button>
)}
</div>
</div>
);
}
Image Processing UI
For tools like background removal and upscaling:function ImageProcessingUI({ entry }: { entry: ProcessedEntry }) {
const [showOriginal, setShowOriginal] = useState(false);
const inputUrl = entry.parsedInput?.data?.image_url as string;
const outputUrl = entry.mediaUrls?.[0];
const toolName = entry.title;
return (
<div style={styles.card}>
{/* Header */}
<div style={styles.header}>
<span style={styles.icon}>✨</span>
<span style={styles.title}>{toolName}</span>
</div>
{/* Before/After comparison */}
<div style={styles.comparisonContainer}>
<div style={styles.comparisonImage}>
<span style={styles.comparisonLabel}>Original</span>
<img src={inputUrl} alt="Original" style={styles.comparisonImg} />
</div>
<div style={styles.comparisonDivider}>→</div>
<div style={styles.comparisonImage}>
<span style={styles.comparisonLabel}>Result</span>
<img src={outputUrl} alt="Result" style={styles.comparisonImg} />
</div>
</div>
{/* Toggle slider */}
<div style={styles.sliderContainer}>
<label style={styles.sliderLabel}>
<input
type="checkbox"
checked={showOriginal}
onChange={(e) => setShowOriginal(e.target.checked)}
/>
Show Original
</label>
</div>
{/* Actions */}
<div style={styles.actions}>
<button onClick={() => downloadImage(outputUrl!)} style={styles.actionButton}>
Download Result
</button>
<button onClick={() => openInEditor(outputUrl!)} style={styles.actionButton}>
Edit Further
</button>
</div>
</div>
);
}
Default Tool UI
Fallback for unknown tools:function DefaultToolUI({ entry }: { entry: ProcessedEntry }) {
return (
<div style={styles.card}>
<div style={styles.header}>
<span style={styles.icon}>⚙️</span>
<span style={styles.title}>{entry.title || entry.displayType}</span>
</div>
{entry.entry.text && (
<div style={styles.content}>
<p>{entry.entry.text}</p>
</div>
)}
{entry.mediaUrls?.map(url => (
<div key={url} style={styles.mediaItem}>
<MediaPreview url={url} />
</div>
))}
</div>
);
}
Usage in Conversation
function Conversation() {
const { processedConversation } = useJadeSession();
return (
<div>
{processedConversation.map((entry, i) => {
// User messages
if (entry.originalType === 'user_text') {
return <UserMessage key={i} text={entry.entry.text} />;
}
// Assistant text
if (entry.originalType === 'assistant_text') {
return <AssistantMessage key={i} entry={entry} />;
}
// Tool calls - use custom UI
if (entry.originalType === 'tool_call') {
return <MessageRenderer key={i} entry={entry} />;
}
return null;
})}
</div>
);
}
Key Concepts
- displayType: Determines which component to render
- parsedInput: Access tool input parameters
- parsedResult: Access tool output data
- mediaUrls: Quick access to generated media
- toolDefinition: Tool metadata (category, icon, etc.)
Related
- ProcessedEntry - Entry structure
- Tools Reference - Available tools
- Basic Chat - Chat interface