Web (React)
'use client';
import { useState, useRef, useEffect } from 'react';
import { JadeProvider, useJadeSession } from '@gr33n-ai/jade-sdk-client';
// Chat component
function Chat() {
const [input, setInput] = useState('');
const messagesEndRef = useRef<HTMLDivElement>(null);
const {
processedConversation,
isStreaming,
streamingText,
sendMessage,
cancel,
} = useJadeSession();
// Auto-scroll to bottom
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [processedConversation, streamingText]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isStreaming) return;
const message = input;
setInput('');
await sendMessage(message);
};
return (
<div style={styles.container}>
{/* Header */}
<header style={styles.header}>
<h1 style={styles.title}>Jade Chat</h1>
</header>
{/* Messages */}
<div style={styles.messages}>
{processedConversation.map((entry, i) => (
<div
key={i}
style={{
...styles.message,
...(entry.originalType === 'user_text' ? styles.userMessage : styles.aiMessage),
}}
>
<div style={styles.messageHeader}>
{entry.originalType === 'user_text' ? 'You' : 'Jade'}
</div>
{entry.entry.text && <p style={styles.messageText}>{entry.entry.text}</p>}
{entry.mediaUrls?.map(url => (
<img key={url} src={url} alt="" style={styles.image} />
))}
</div>
))}
{/* Streaming text */}
{streamingText && (
<div style={{ ...styles.message, ...styles.aiMessage }}>
<div style={styles.messageHeader}>Jade</div>
<p style={styles.messageText}>{streamingText}</p>
</div>
)}
{/* Loading indicator */}
{isStreaming && !streamingText && (
<div style={styles.loading}>Thinking...</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<form onSubmit={handleSubmit} style={styles.inputForm}>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message..."
disabled={isStreaming}
style={styles.input}
/>
{isStreaming ? (
<button type="button" onClick={cancel} style={styles.cancelButton}>
Cancel
</button>
) : (
<button type="submit" disabled={!input.trim()} style={styles.sendButton}>
Send
</button>
)}
</form>
</div>
);
}
// Main App
export default function App() {
return (
<JadeProvider
config={{
endpoint: 'https://api.gr33n.ai',
getAuthToken: () => process.env.NEXT_PUBLIC_JADE_API_KEY!,
}}
>
<Chat />
</JadeProvider>
);
}
// Styles
const styles: Record<string, React.CSSProperties> = {
container: {
display: 'flex',
flexDirection: 'column',
height: '100vh',
maxWidth: 800,
margin: '0 auto',
},
header: {
padding: 16,
borderBottom: '1px solid #eee',
},
title: {
margin: 0,
fontSize: 24,
},
messages: {
flex: 1,
overflowY: 'auto',
padding: 16,
},
message: {
marginBottom: 16,
padding: 12,
borderRadius: 12,
maxWidth: '80%',
},
userMessage: {
marginLeft: 'auto',
backgroundColor: '#e3f2fd',
},
aiMessage: {
marginRight: 'auto',
backgroundColor: '#f5f5f5',
},
messageHeader: {
fontWeight: 'bold',
marginBottom: 4,
fontSize: 14,
},
messageText: {
margin: 0,
},
image: {
maxWidth: '100%',
borderRadius: 8,
marginTop: 8,
},
loading: {
textAlign: 'center',
color: '#666',
padding: 16,
},
inputForm: {
display: 'flex',
gap: 8,
padding: 16,
borderTop: '1px solid #eee',
},
input: {
flex: 1,
padding: 12,
border: '1px solid #ddd',
borderRadius: 8,
fontSize: 16,
},
sendButton: {
padding: '12px 24px',
backgroundColor: '#10B981',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
},
cancelButton: {
padding: '12px 24px',
backgroundColor: '#ef4444',
color: 'white',
border: 'none',
borderRadius: 8,
cursor: 'pointer',
fontSize: 16,
},
};
React Native
import { useState, useRef } from 'react';
import {
View,
Text,
TextInput,
FlatList,
TouchableOpacity,
Image,
KeyboardAvoidingView,
Platform,
StyleSheet,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import {
JadeProvider,
useJadeSession,
createAsyncStorageAdapter,
} from '@gr33n-ai/jade-sdk-rn-client';
import AsyncStorage from '@react-native-async-storage/async-storage';
const storage = createAsyncStorageAdapter(AsyncStorage);
function Chat() {
const [input, setInput] = useState('');
const flatListRef = useRef<FlatList>(null);
const {
processedConversation,
isStreaming,
streamingText,
sendMessage,
cancel,
} = useJadeSession();
const handleSend = async () => {
if (!input.trim() || isStreaming) return;
const message = input;
setInput('');
await sendMessage(message);
};
const renderMessage = ({ item, index }: { item: any; index: number }) => (
<View
style={[
styles.message,
item.originalType === 'user_text' ? styles.userMessage : styles.aiMessage,
]}
>
<Text style={styles.messageHeader}>
{item.originalType === 'user_text' ? 'You' : 'Jade'}
</Text>
{item.entry.text && <Text>{item.entry.text}</Text>}
{item.mediaUrls?.map((url: string) => (
<Image
key={url}
source={{ uri: url }}
style={styles.image}
resizeMode="cover"
/>
))}
</View>
);
// Combine conversation with streaming
const displayData = [
...processedConversation,
...(streamingText ? [{
id: 'streaming',
originalType: 'assistant_text',
entry: { text: streamingText },
}] : []),
];
return (
<SafeAreaView style={styles.container}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Jade Chat</Text>
</View>
{/* Messages */}
<FlatList
ref={flatListRef}
data={displayData}
renderItem={renderMessage}
keyExtractor={(_, i) => i.toString()}
style={styles.messages}
contentContainerStyle={styles.messagesContent}
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
ListFooterComponent={
isStreaming && !streamingText ? (
<Text style={styles.loading}>Thinking...</Text>
) : null
}
/>
{/* Input */}
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={input}
onChangeText={setInput}
placeholder="Type a message..."
editable={!isStreaming}
multiline
/>
{isStreaming ? (
<TouchableOpacity onPress={cancel} style={styles.cancelButton}>
<Text style={styles.buttonText}>Cancel</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
onPress={handleSend}
style={[styles.sendButton, !input.trim() && styles.buttonDisabled]}
disabled={!input.trim()}
>
<Text style={styles.buttonText}>Send</Text>
</TouchableOpacity>
)}
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
export default function App() {
return (
<JadeProvider
config={{
endpoint: 'https://api.gr33n.ai',
getAuthToken: async () => await AsyncStorage.getItem('@gr33n-ai/api-key'),
}}
storage={storage}
>
<Chat />
</JadeProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
messages: {
flex: 1,
},
messagesContent: {
padding: 16,
},
message: {
marginBottom: 16,
padding: 12,
borderRadius: 12,
maxWidth: '80%',
},
userMessage: {
alignSelf: 'flex-end',
backgroundColor: '#e3f2fd',
},
aiMessage: {
alignSelf: 'flex-start',
backgroundColor: '#f5f5f5',
},
messageHeader: {
fontWeight: 'bold',
marginBottom: 4,
fontSize: 14,
},
image: {
width: '100%',
height: 200,
borderRadius: 8,
marginTop: 8,
},
loading: {
textAlign: 'center',
color: '#666',
padding: 16,
},
inputContainer: {
flexDirection: 'row',
padding: 16,
gap: 8,
borderTopWidth: 1,
borderTopColor: '#eee',
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
maxHeight: 100,
fontSize: 16,
},
sendButton: {
backgroundColor: '#10B981',
paddingHorizontal: 20,
borderRadius: 8,
justifyContent: 'center',
},
cancelButton: {
backgroundColor: '#ef4444',
paddingHorizontal: 20,
borderRadius: 8,
justifyContent: 'center',
},
buttonDisabled: {
backgroundColor: '#ccc',
},
buttonText: {
color: '#fff',
fontWeight: 'bold',
fontSize: 16,
},
});
Key Features
This example demonstrates:- Real-time streaming text display
- Message bubbles with user/AI differentiation
- Image display for generated media
- Loading and error states
- Cancel button during streaming
- Auto-scroll to latest message
- Keyboard handling (React Native)
Next Steps
Media Gallery
Add a dedicated media gallery view.
Custom Tool UI
Build specialized UI for different tools.