Web (React)
'use client';
import { useState } from 'react';
import { useJadeSession, useMedia } from '@gr33n-ai/jade-sdk-client';
import type { MediaInfo } from '@gr33n-ai/jade-sdk-client';
// Gallery component
function MediaGallery() {
const { media } = useJadeSession();
const [selectedMedia, setSelectedMedia] = useState<MediaInfo | null>(null);
const [filter, setFilter] = useState<'all' | 'image' | 'video' | 'audio'>('all');
const filteredMedia = filter === 'all'
? media
: media.filter(m => m.type === filter);
return (
<div style={styles.container}>
{/* Filter tabs */}
<div style={styles.filters}>
{(['all', 'image', 'video', 'audio'] as const).map(f => (
<button
key={f}
onClick={() => setFilter(f)}
style={{
...styles.filterButton,
...(filter === f ? styles.filterButtonActive : {}),
}}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
{f !== 'all' && (
<span style={styles.count}>
{media.filter(m => m.type === f).length}
</span>
)}
</button>
))}
</div>
{/* Grid */}
{filteredMedia.length === 0 ? (
<div style={styles.empty}>
<p>No media generated yet</p>
<p style={styles.hint}>Ask Jade to generate images, videos, or audio</p>
</div>
) : (
<div style={styles.grid}>
{filteredMedia.map((item, i) => (
<MediaCard
key={item.url}
item={item}
onClick={() => setSelectedMedia(item)}
/>
))}
</div>
)}
{/* Lightbox */}
{selectedMedia && (
<Lightbox
media={selectedMedia}
onClose={() => setSelectedMedia(null)}
onNext={() => {
const idx = filteredMedia.findIndex(m => m.url === selectedMedia.url);
if (idx < filteredMedia.length - 1) {
setSelectedMedia(filteredMedia[idx + 1]);
}
}}
onPrev={() => {
const idx = filteredMedia.findIndex(m => m.url === selectedMedia.url);
if (idx > 0) {
setSelectedMedia(filteredMedia[idx - 1]);
}
}}
/>
)}
</div>
);
}
// Media card component
function MediaCard({ item, onClick }: { item: MediaInfo; onClick: () => void }) {
return (
<div style={styles.card} onClick={onClick}>
{item.type === 'image' && (
<img src={item.url} alt={item.prompt || ''} style={styles.thumbnail} />
)}
{item.type === 'video' && (
<div style={styles.videoThumb}>
<video src={item.url} style={styles.thumbnail} />
<div style={styles.playIcon}>▶</div>
</div>
)}
{item.type === 'audio' && (
<div style={styles.audioThumb}>
<span style={styles.audioIcon}>🎵</span>
</div>
)}
<div style={styles.cardInfo}>
<span style={styles.mediaType}>{item.type}</span>
{item.prompt && (
<p style={styles.prompt}>{item.prompt.slice(0, 50)}...</p>
)}
</div>
</div>
);
}
// Lightbox component
function Lightbox({
media,
onClose,
onNext,
onPrev,
}: {
media: MediaInfo;
onClose: () => void;
onNext: () => void;
onPrev: () => void;
}) {
const download = async () => {
const response = await fetch(media.url);
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `jade-${media.type}-${Date.now()}.${getExtension(media.type)}`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={styles.lightbox} onClick={onClose}>
<div style={styles.lightboxContent} onClick={e => e.stopPropagation()}>
{/* Navigation */}
<button onClick={onPrev} style={styles.navButton}>←</button>
{/* Media */}
<div style={styles.mediaContainer}>
{media.type === 'image' && (
<img src={media.url} alt={media.prompt || ''} style={styles.fullMedia} />
)}
{media.type === 'video' && (
<video src={media.url} controls style={styles.fullMedia} />
)}
{media.type === 'audio' && (
<audio src={media.url} controls style={styles.audioPlayer} />
)}
</div>
<button onClick={onNext} style={styles.navButton}>→</button>
{/* Close button */}
<button onClick={onClose} style={styles.closeButton}>×</button>
{/* Info panel */}
<div style={styles.infoPanel}>
{media.prompt && <p><strong>Prompt:</strong> {media.prompt}</p>}
{media.model && <p><strong>Model:</strong> {media.model}</p>}
{media.aspectRatio && <p><strong>Aspect:</strong> {media.aspectRatio}</p>}
<p><strong>Created:</strong> {new Date(media.timestamp).toLocaleString()}</p>
<button onClick={download} style={styles.downloadButton}>
Download
</button>
</div>
</div>
</div>
);
}
function getExtension(type: string): string {
switch (type) {
case 'image': return 'png';
case 'video': return 'mp4';
case 'audio': return 'mp3';
default: return 'bin';
}
}
// Styles
const styles: Record<string, React.CSSProperties> = {
container: {
padding: 20,
},
filters: {
display: 'flex',
gap: 8,
marginBottom: 20,
},
filterButton: {
padding: '8px 16px',
border: '1px solid #ddd',
borderRadius: 20,
background: 'white',
cursor: 'pointer',
},
filterButtonActive: {
background: '#10B981',
color: 'white',
borderColor: '#10B981',
},
count: {
marginLeft: 8,
opacity: 0.7,
},
empty: {
textAlign: 'center',
padding: 60,
color: '#666',
},
hint: {
fontSize: 14,
opacity: 0.7,
},
grid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
gap: 16,
},
card: {
borderRadius: 12,
overflow: 'hidden',
border: '1px solid #eee',
cursor: 'pointer',
transition: 'transform 0.2s',
},
thumbnail: {
width: '100%',
height: 150,
objectFit: 'cover',
},
videoThumb: {
position: 'relative',
},
playIcon: {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'rgba(0,0,0,0.5)',
color: 'white',
borderRadius: '50%',
width: 40,
height: 40,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
audioThumb: {
height: 150,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f5f5f5',
},
audioIcon: {
fontSize: 48,
},
cardInfo: {
padding: 12,
},
mediaType: {
fontSize: 12,
textTransform: 'uppercase',
color: '#666',
},
prompt: {
fontSize: 14,
margin: '8px 0 0',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
lightbox: {
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.9)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
},
lightboxContent: {
display: 'flex',
alignItems: 'center',
gap: 20,
maxWidth: '90vw',
maxHeight: '90vh',
},
mediaContainer: {
maxWidth: '70vw',
maxHeight: '80vh',
},
fullMedia: {
maxWidth: '100%',
maxHeight: '80vh',
borderRadius: 8,
},
audioPlayer: {
width: 400,
},
navButton: {
padding: 20,
background: 'rgba(255,255,255,0.1)',
border: 'none',
color: 'white',
fontSize: 24,
cursor: 'pointer',
borderRadius: 8,
},
closeButton: {
position: 'absolute',
top: 20,
right: 20,
background: 'none',
border: 'none',
color: 'white',
fontSize: 32,
cursor: 'pointer',
},
infoPanel: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
background: 'rgba(0,0,0,0.7)',
color: 'white',
padding: 16,
borderRadius: 8,
},
downloadButton: {
marginTop: 12,
padding: '8px 16px',
background: '#10B981',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
},
};
export default MediaGallery;
React Native
import { useState } from 'react';
import {
View,
Text,
FlatList,
Image,
TouchableOpacity,
Modal,
StyleSheet,
Dimensions,
} from 'react-native';
import { useJadeSession } from '@gr33n-ai/jade-sdk-rn-client';
import Video from 'react-native-video';
import type { MediaInfo } from '@gr33n-ai/jade-sdk-rn-client';
const { width } = Dimensions.get('window');
const ITEM_SIZE = (width - 48) / 3;
function MediaGallery() {
const { media } = useJadeSession();
const [selectedMedia, setSelectedMedia] = useState<MediaInfo | null>(null);
const [filter, setFilter] = useState<'all' | 'image' | 'video' | 'audio'>('all');
const filteredMedia = filter === 'all'
? media
: media.filter(m => m.type === filter);
const renderItem = ({ item }: { item: MediaInfo }) => (
<TouchableOpacity
style={styles.gridItem}
onPress={() => setSelectedMedia(item)}
>
{item.type === 'image' && (
<Image source={{ uri: item.url }} style={styles.thumbnail} />
)}
{item.type === 'video' && (
<View style={styles.videoThumb}>
<Image source={{ uri: item.url }} style={styles.thumbnail} />
<View style={styles.playIcon}>
<Text style={styles.playText}>▶</Text>
</View>
</View>
)}
{item.type === 'audio' && (
<View style={styles.audioThumb}>
<Text style={styles.audioIcon}>🎵</Text>
</View>
)}
</TouchableOpacity>
);
return (
<View style={styles.container}>
{/* Filters */}
<View style={styles.filters}>
{(['all', 'image', 'video', 'audio'] as const).map(f => (
<TouchableOpacity
key={f}
onPress={() => setFilter(f)}
style={[styles.filterButton, filter === f && styles.filterActive]}
>
<Text style={[styles.filterText, filter === f && styles.filterTextActive]}>
{f.charAt(0).toUpperCase() + f.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
{/* Grid */}
{filteredMedia.length === 0 ? (
<View style={styles.empty}>
<Text style={styles.emptyText}>No media generated yet</Text>
</View>
) : (
<FlatList
data={filteredMedia}
renderItem={renderItem}
keyExtractor={item => item.url}
numColumns={3}
contentContainerStyle={styles.grid}
/>
)}
{/* Lightbox Modal */}
<Modal
visible={!!selectedMedia}
transparent
animationType="fade"
onRequestClose={() => setSelectedMedia(null)}
>
{selectedMedia && (
<View style={styles.modal}>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setSelectedMedia(null)}
>
<Text style={styles.closeText}>×</Text>
</TouchableOpacity>
{selectedMedia.type === 'image' && (
<Image
source={{ uri: selectedMedia.url }}
style={styles.fullImage}
resizeMode="contain"
/>
)}
{selectedMedia.type === 'video' && (
<Video
source={{ uri: selectedMedia.url }}
style={styles.fullVideo}
controls
resizeMode="contain"
/>
)}
<View style={styles.infoPanel}>
{selectedMedia.prompt && (
<Text style={styles.infoText} numberOfLines={2}>
{selectedMedia.prompt}
</Text>
)}
<Text style={styles.infoMeta}>
{selectedMedia.model} • {new Date(selectedMedia.timestamp).toLocaleDateString()}
</Text>
</View>
</View>
)}
</Modal>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
filters: {
flexDirection: 'row',
padding: 16,
gap: 8,
},
filterButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: '#ddd',
},
filterActive: {
backgroundColor: '#10B981',
borderColor: '#10B981',
},
filterText: { color: '#666' },
filterTextActive: { color: '#fff' },
empty: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
emptyText: { color: '#666' },
grid: { padding: 16 },
gridItem: {
width: ITEM_SIZE,
height: ITEM_SIZE,
margin: 4,
borderRadius: 8,
overflow: 'hidden',
},
thumbnail: {
width: '100%',
height: '100%',
},
videoThumb: { position: 'relative' },
playIcon: {
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: -15 }, { translateY: -15 }],
width: 30,
height: 30,
borderRadius: 15,
backgroundColor: 'rgba(0,0,0,0.5)',
alignItems: 'center',
justifyContent: 'center',
},
playText: { color: '#fff' },
audioThumb: {
backgroundColor: '#f5f5f5',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
},
audioIcon: { fontSize: 32 },
modal: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.9)',
justifyContent: 'center',
},
closeButton: {
position: 'absolute',
top: 50,
right: 20,
zIndex: 1,
},
closeText: { color: '#fff', fontSize: 32 },
fullImage: { width: '100%', height: '70%' },
fullVideo: { width: '100%', height: '70%' },
infoPanel: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 20,
backgroundColor: 'rgba(0,0,0,0.7)',
},
infoText: { color: '#fff', fontSize: 16 },
infoMeta: { color: '#aaa', fontSize: 12, marginTop: 8 },
});
export default MediaGallery;
Key Features
- Type Filtering: Filter by image, video, or audio
- Grid Layout: Responsive thumbnail grid
- Lightbox: Full-screen media viewing
- Navigation: Next/previous navigation
- Download: Download generated media
- Metadata: Display prompt, model, timestamp
Related
- Media Handling Guide - Display patterns
- useMedia Hook - Media extraction
- Basic Chat - Chat interface