import { View, Text, Pressable, Animated } from 'react-native'; import { useState, useEffect, useRef } from 'react'; interface UserMessageProps { message: string; timestamp: number; } export function UserMessage({ message, timestamp }: UserMessageProps) { const time = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); return ( {message} {time} ); } interface AssistantMessageProps { message: string; timestamp: number; isStreaming?: boolean; } export function AssistantMessage({ message, timestamp, isStreaming = false }: AssistantMessageProps) { const time = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 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]); return ( {message} {time} {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; } export function ActivityLog({ type, message, timestamp, metadata, artifactId, artifactType, title, onArtifactClick }: ActivityLogProps) { const [isExpanded, setIsExpanded] = useState(false); const typeConfig = { system: { bg: 'bg-zinc-800/50', text: 'text-zinc-400', icon: '●' }, info: { bg: 'bg-blue-900/30', text: 'text-blue-400', icon: 'i' }, success: { bg: 'bg-green-900/30', text: 'text-green-400', icon: '✓' }, error: { bg: 'bg-red-900/30', text: 'text-red-400', icon: '✗' }, artifact: { bg: 'bg-blue-900/40', text: 'text-blue-300', icon: '📋' }, }; const config = typeConfig[type]; const handlePress = () => { if (type === 'artifact' && artifactId && onArtifactClick) { onArtifactClick(artifactId); } else if (metadata) { setIsExpanded(!isExpanded); } }; const displayMessage = type === 'artifact' && artifactType && title ? `${artifactType}: ${title}` : message; return ( {config.icon} {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'; } 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: 'border-amber-500', bg: 'bg-amber-500/20', text: 'text-amber-300', icon: '⟳', label: 'executing', }, completed: { border: 'border-green-500', bg: 'bg-green-500/20', text: 'text-green-300', icon: '✓', label: 'completed', }, failed: { border: 'border-red-500', bg: 'bg-red-500/20', text: 'text-red-300', icon: '✗', label: 'failed', }, }; const config = statusConfig[status]; return ( setIsExpanded(!isExpanded)} className={`mx-2 mb-2 bg-zinc-900 rounded-lg border ${config.border} overflow-hidden active:opacity-80`} > {isExpanded ? '▼' : '▶'} {toolName} {status === 'executing' ? ( {config.icon} ) : ( {config.icon} )} {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)} )} )} ); }