Extracting Media
TheuseJadeSession hook automatically extracts all media from the conversation:
const { media, processedConversation } = useJadeSession();
// media: MediaInfo[]
// - All unique media URLs
// - Sorted by timestamp (most recent first)
// - Enriched with metadata (prompt, model, type)
MediaInfo Structure
interface MediaInfo {
url: string; // CDN URL
type: 'image' | 'video' | 'audio'; // Media type
timestamp: string; // When generated
toolName?: string; // Tool that created it
prompt?: string; // Generation prompt
model?: string; // AI model used
aspectRatio?: string; // e.g., "16:9"
}
Displaying Images
Basic Display
function ImageGallery() {
const { media } = useJadeSession();
const images = media.filter(m => m.type === 'image');
return (
<div className="gallery">
{images.map(img => (
<img
key={img.url}
src={img.url}
alt={img.prompt || 'Generated image'}
loading="lazy"
/>
))}
</div>
);
}
With Loading States
function ImageWithLoading({ src, alt }: { src: string; alt: string }) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className="image-container">
{!loaded && !error && <div className="skeleton" />}
{error && <div className="error">Failed to load</div>}
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
style={{ display: loaded ? 'block' : 'none' }}
/>
</div>
);
}
React Native
import { Image, ActivityIndicator } from 'react-native';
function RNImageGallery() {
const { media } = useJadeSession();
const images = media.filter(m => m.type === 'image');
return (
<FlatList
data={images}
keyExtractor={item => item.url}
numColumns={2}
renderItem={({ item }) => (
<Image
source={{ uri: item.url }}
style={{ width: 150, height: 150 }}
resizeMode="cover"
/>
)}
/>
);
}
Displaying Videos
Web
function VideoPlayer({ video }: { video: MediaInfo }) {
return (
<video
src={video.url}
controls
playsInline
style={{ maxWidth: '100%' }}
>
Your browser doesn't support video.
</video>
);
}
React Native
import Video from 'react-native-video';
function RNVideoPlayer({ video }: { video: MediaInfo }) {
return (
<Video
source={{ uri: video.url }}
style={{ width: '100%', height: 200 }}
controls
resizeMode="contain"
/>
);
}
Displaying Audio
Web
function AudioPlayer({ audio }: { audio: MediaInfo }) {
return (
<div className="audio-player">
<audio src={audio.url} controls />
<p>{audio.prompt}</p>
</div>
);
}
React Native
import Sound from 'react-native-sound';
function useAudioPlayer(url: string) {
const [playing, setPlaying] = useState(false);
const soundRef = useRef<Sound | null>(null);
useEffect(() => {
soundRef.current = new Sound(url, '', (error) => {
if (error) console.error('Failed to load sound', error);
});
return () => soundRef.current?.release();
}, [url]);
const play = () => {
soundRef.current?.play(() => setPlaying(false));
setPlaying(true);
};
const stop = () => {
soundRef.current?.stop();
setPlaying(false);
};
return { playing, play, stop };
}
Media in Conversation
Media appears inprocessedConversation entries:
function ConversationEntry({ entry }: { entry: ProcessedEntry }) {
return (
<div>
{/* Text content */}
{entry.entry.text && <p>{entry.entry.text}</p>}
{/* Inline media */}
{entry.mediaUrls?.map(url => {
const type = getMediaType(url);
if (type === 'image') return <img key={url} src={url} />;
if (type === 'video') return <video key={url} src={url} controls />;
if (type === 'audio') return <audio key={url} src={url} controls />;
})}
</div>
);
}
function getMediaType(url: string): 'image' | 'video' | 'audio' {
if (url.match(/\.(jpg|jpeg|png|gif|webp)$/i)) return 'image';
if (url.match(/\.(mp4|webm|mov)$/i)) return 'video';
if (url.match(/\.(mp3|wav|m4a)$/i)) return 'audio';
return 'image'; // default
}
Media Callbacks
React to media generation events:const { media } = useJadeSession({
onMediaGenerated: (urls, type) => {
// Show toast notification
toast(`Generated ${urls.length} ${type}(s)!`);
// Auto-download
if (type === 'image') {
urls.forEach(url => downloadImage(url));
}
// Update gallery
refreshGallery();
},
});
Downloading Media
Web
async function downloadMedia(url: string, filename: string) {
const response = await fetch(url);
const blob = await response.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
React Native
import RNFetchBlob from 'rn-fetch-blob';
async function downloadMedia(url: string) {
const { config, fs } = RNFetchBlob;
const date = new Date();
const filename = `jade_${date.getTime()}.jpg`;
const res = await config({
fileCache: true,
addAndroidDownloads: {
useDownloadManager: true,
notification: true,
path: `${fs.dirs.DownloadDir}/${filename}`,
},
}).fetch('GET', url);
return res.path();
}
Caching
React Native with FastImage
import FastImage from 'react-native-fast-image';
function CachedImage({ url }: { url: string }) {
return (
<FastImage
source={{
uri: url,
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
style={{ width: 200, height: 200 }}
resizeMode={FastImage.resizeMode.cover}
/>
);
}
Best Practices
Use
loading="lazy" for images below the foldShow loading skeletons while media loads
Handle load errors gracefully with fallback UI
Use appropriate resolutions (thumbnails vs full-size)
Cache media on mobile for offline access
Provide download functionality for generated content