import { View, Text, Pressable, Animated } from 'react-native'; import { useState, useEffect, useRef } from 'react'; import Markdown from 'react-native-markdown-display'; import { Circle, Info, CheckCircle, XCircle, FileText, ChevronRight, ChevronDown, RefreshCw } from 'lucide-react-native'; import { StyleSheet } from 'react-native-unistyles'; interface UserMessageProps { message: string; timestamp: number; } const userMessageStylesheet = StyleSheet.create((theme) => ({ container: { flexDirection: 'row', justifyContent: 'flex-end', marginBottom: theme.spacing[3], paddingHorizontal: theme.spacing[4], }, bubble: { backgroundColor: theme.colors.primary, borderRadius: theme.borderRadius['2xl'], borderTopRightRadius: theme.borderRadius.sm, paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], maxWidth: '80%', }, text: { color: theme.colors.primaryForeground, fontSize: theme.fontSize.lg, lineHeight: 24, }, })); export function UserMessage({ message, timestamp }: UserMessageProps) { return ( {message} ); } interface AssistantMessageProps { message: string; timestamp: number; isStreaming?: boolean; } const assistantMessageStylesheet = StyleSheet.create((theme) => ({ container: { marginBottom: theme.spacing[3], paddingHorizontal: theme.spacing[4], paddingVertical: theme.spacing[3], }, streamingIndicator: { marginTop: theme.spacing[1], }, streamingText: { color: theme.colors.palette.teal[200], fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.bold, }, })); export function AssistantMessage({ message, timestamp, isStreaming = false }: AssistantMessageProps) { const fadeAnim = useRef(new Animated.Value(0.3)).current; useEffect(() => { if (isStreaming) { Animated.loop( Animated.sequence([ Animated.timing(fadeAnim, { toValue: 1, duration: 600, useNativeDriver: true, }), Animated.timing(fadeAnim, { toValue: 0.3, duration: 600, useNativeDriver: true, }), ]) ).start(); } else { fadeAnim.stopAnimation(); fadeAnim.setValue(1); } }, [isStreaming, fadeAnim]); const markdownStyles = { body: { color: '#f0fdfa', fontSize: 16, lineHeight: 24, }, paragraph: { marginTop: 0, marginBottom: 8, }, strong: { fontWeight: '700' as const, }, em: { fontStyle: 'italic' as const, }, code_inline: { backgroundColor: '#134e4a', color: '#ccfbf1', paddingHorizontal: 4, paddingVertical: 2, borderRadius: 3, fontFamily: 'monospace', }, code_block: { backgroundColor: '#134e4a', color: '#ccfbf1', padding: 12, borderRadius: 6, fontFamily: 'monospace', fontSize: 14, }, fence: { backgroundColor: '#134e4a', color: '#ccfbf1', padding: 12, borderRadius: 6, fontFamily: 'monospace', fontSize: 14, }, link: { color: '#5eead4', textDecorationLine: 'underline' as const, }, bullet_list: { marginBottom: 8, }, ordered_list: { marginBottom: 8, }, list_item: { marginBottom: 4, }, }; return ( {message} {isStreaming && ( ... )} ); } interface ActivityLogProps { type: 'system' | 'info' | 'success' | 'error' | 'artifact'; message: string; timestamp: number; metadata?: Record; artifactId?: string; artifactType?: string; title?: string; onArtifactClick?: (artifactId: string) => void; } const activityLogStylesheet = StyleSheet.create((theme) => ({ pressable: { marginHorizontal: theme.spacing[2], marginBottom: theme.spacing[1], borderRadius: theme.borderRadius.md, overflow: 'hidden', }, pressableActive: { opacity: 0.7, }, systemBg: { backgroundColor: 'rgba(39, 39, 42, 0.5)', }, infoBg: { backgroundColor: 'rgba(30, 58, 138, 0.3)', }, successBg: { backgroundColor: 'rgba(20, 83, 45, 0.3)', }, errorBg: { backgroundColor: 'rgba(127, 29, 29, 0.3)', }, artifactBg: { backgroundColor: 'rgba(30, 58, 138, 0.4)', }, content: { paddingHorizontal: theme.spacing[3], paddingVertical: 10, }, row: { flexDirection: 'row', alignItems: 'flex-start', gap: theme.spacing[2], }, iconContainer: { flexShrink: 0, }, textContainer: { flex: 1, }, messageText: { fontSize: theme.fontSize.sm, lineHeight: 20, }, detailsRow: { flexDirection: 'row', alignItems: 'center', marginTop: theme.spacing[1], }, detailsText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, marginRight: theme.spacing[1], }, metadataContainer: { marginTop: theme.spacing[2], backgroundColor: 'rgba(0, 0, 0, 0.5)', borderRadius: theme.borderRadius.base, padding: theme.spacing[2], borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, }, metadataText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, fontFamily: 'monospace', lineHeight: 16, }, })); export function ActivityLog({ type, message, timestamp, metadata, artifactId, artifactType, title, onArtifactClick }: ActivityLogProps) { const [isExpanded, setIsExpanded] = useState(false); const typeConfig = { system: { bg: activityLogStylesheet.systemBg, color: '#a1a1aa', Icon: Circle }, info: { bg: activityLogStylesheet.infoBg, color: '#60a5fa', Icon: Info }, success: { bg: activityLogStylesheet.successBg, color: '#4ade80', Icon: CheckCircle }, error: { bg: activityLogStylesheet.errorBg, color: '#f87171', Icon: XCircle }, artifact: { bg: activityLogStylesheet.artifactBg, color: '#93c5fd', Icon: FileText }, }; const config = typeConfig[type]; const IconComponent = config.Icon; const handlePress = () => { if (type === 'artifact' && artifactId && onArtifactClick) { onArtifactClick(artifactId); } else if (metadata) { setIsExpanded(!isExpanded); } }; const displayMessage = type === 'artifact' && artifactType && title ? `${artifactType}: ${title}` : message; const isInteractive = type === 'artifact' || metadata; return ( {displayMessage} {metadata && ( Details {isExpanded ? ( ) : ( )} )} {isExpanded && metadata && ( {JSON.stringify(metadata, null, 2)} )} ); } interface ToolCallProps { toolName: string; args: any; result?: any; error?: any; status: 'executing' | 'completed' | 'failed'; } const toolCallStylesheet = StyleSheet.create((theme) => ({ pressable: { marginHorizontal: theme.spacing[2], marginBottom: theme.spacing[2], backgroundColor: theme.colors.card, borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], overflow: 'hidden', }, pressableActive: { opacity: 0.8, }, executingBorder: { borderColor: theme.colors.palette.amber[500], }, completedBorder: { borderColor: theme.colors.palette.green[500], }, failedBorder: { borderColor: theme.colors.destructive, }, content: { padding: theme.spacing[3], }, headerRow: { flexDirection: 'row', alignItems: 'center', }, chevronContainer: { marginRight: theme.spacing[2], }, toolName: { color: theme.colors.foreground, fontFamily: 'monospace', fontWeight: theme.fontWeight.semibold, fontSize: theme.fontSize.sm, flex: 1, }, statusBadge: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: theme.spacing[2], paddingVertical: theme.spacing[1], borderRadius: theme.borderRadius.base, }, executingBadgeBg: { backgroundColor: 'rgba(245, 158, 11, 0.2)', }, completedBadgeBg: { backgroundColor: 'rgba(34, 197, 94, 0.2)', }, failedBadgeBg: { backgroundColor: 'rgba(239, 68, 68, 0.2)', }, statusText: { fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, textTransform: 'uppercase', }, expandedContent: { marginTop: theme.spacing[3], gap: theme.spacing[2], }, section: { // empty - just for grouping }, sectionTitle: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.semibold, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 6, }, errorSectionTitle: { color: theme.colors.palette.red[300], }, sectionContent: { backgroundColor: theme.colors.background, borderRadius: theme.borderRadius.base, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, padding: theme.spacing[2], }, errorSectionContent: { borderColor: theme.colors.palette.red[800], }, sectionText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, fontFamily: 'monospace', lineHeight: 16, }, })); export function ToolCall({ toolName, args, result, error, status }: ToolCallProps) { const [isExpanded, setIsExpanded] = useState(false); const spinAnim = useRef(new Animated.Value(0)).current; useEffect(() => { if (status === 'executing') { Animated.loop( Animated.timing(spinAnim, { toValue: 1, duration: 1000, useNativeDriver: true, }) ).start(); } else { spinAnim.stopAnimation(); spinAnim.setValue(0); } }, [status, spinAnim]); const spin = spinAnim.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '360deg'], }); const statusConfig = { executing: { border: toolCallStylesheet.executingBorder, badgeBg: toolCallStylesheet.executingBadgeBg, color: '#fcd34d', label: 'executing', }, completed: { border: toolCallStylesheet.completedBorder, badgeBg: toolCallStylesheet.completedBadgeBg, color: '#86efac', label: 'completed', }, failed: { border: toolCallStylesheet.failedBorder, badgeBg: toolCallStylesheet.failedBadgeBg, color: '#fca5a5', label: 'failed', }, }; const config = statusConfig[status]; return ( setIsExpanded(!isExpanded)} style={[toolCallStylesheet.pressable, toolCallStylesheet.pressableActive, config.border]} > {isExpanded ? ( ) : ( )} {toolName} {status === 'executing' ? ( ) : status === 'completed' ? ( ) : ( )} {config.label} {isExpanded && ( Arguments {JSON.stringify(args, null, 2)} {result !== undefined && ( Result {JSON.stringify(result, null, 2)} )} {error !== undefined && ( Error {JSON.stringify(error, null, 2)} )} )} ); }