feat: refactor app structure and components for improved organization and functionality
- Moved components and hooks into a new directory structure under `src/app` for better organization. - Updated the main entry point to use `index.ts` and adjusted routing accordingly. - Introduced new components such as `ActiveProcesses`, `AgentStreamView`, and `ConnectionStatus` to enhance the app's functionality. - Removed deprecated components and hooks to streamline the codebase. - Updated styles to align with the new theme structure and ensure consistency across the app. This refactor aims to improve maintainability and scalability of the application.
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
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 (
|
||||
<View style={userMessageStylesheet.container}>
|
||||
<View style={userMessageStylesheet.bubble}>
|
||||
<Text style={userMessageStylesheet.text}>{message}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={assistantMessageStylesheet.container}>
|
||||
<Markdown style={markdownStyles}>{message}</Markdown>
|
||||
{isStreaming && (
|
||||
<Animated.View style={[assistantMessageStylesheet.streamingIndicator, { opacity: fadeAnim }]}>
|
||||
<Text style={assistantMessageStylesheet.streamingText}>...</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface ActivityLogProps {
|
||||
type: 'system' | 'info' | 'success' | 'error' | 'artifact';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
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 (
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
disabled={!isInteractive}
|
||||
style={[
|
||||
activityLogStylesheet.pressable,
|
||||
config.bg,
|
||||
isInteractive && activityLogStylesheet.pressableActive,
|
||||
]}
|
||||
>
|
||||
<View style={activityLogStylesheet.content}>
|
||||
<View style={activityLogStylesheet.row}>
|
||||
<View style={activityLogStylesheet.iconContainer}>
|
||||
<IconComponent size={16} color={config.color} />
|
||||
</View>
|
||||
<View style={activityLogStylesheet.textContainer}>
|
||||
<Text style={[activityLogStylesheet.messageText, { color: config.color }]}>
|
||||
{displayMessage}
|
||||
</Text>
|
||||
{metadata && (
|
||||
<View style={activityLogStylesheet.detailsRow}>
|
||||
<Text style={activityLogStylesheet.detailsText}>Details</Text>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={12} color="#71717a" />
|
||||
) : (
|
||||
<ChevronRight size={12} color="#71717a" />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{isExpanded && metadata && (
|
||||
<View style={activityLogStylesheet.metadataContainer}>
|
||||
<Text style={activityLogStylesheet.metadataText}>
|
||||
{JSON.stringify(metadata, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Pressable
|
||||
onPress={() => setIsExpanded(!isExpanded)}
|
||||
style={[toolCallStylesheet.pressable, toolCallStylesheet.pressableActive, config.border]}
|
||||
>
|
||||
<View style={toolCallStylesheet.content}>
|
||||
<View style={toolCallStylesheet.headerRow}>
|
||||
<View style={toolCallStylesheet.chevronContainer}>
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={16} color="#9ca3af" />
|
||||
) : (
|
||||
<ChevronRight size={16} color="#9ca3af" />
|
||||
)}
|
||||
</View>
|
||||
<Text style={toolCallStylesheet.toolName}>
|
||||
{toolName}
|
||||
</Text>
|
||||
<View style={[toolCallStylesheet.statusBadge, config.badgeBg]}>
|
||||
{status === 'executing' ? (
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<RefreshCw size={14} color={config.color} />
|
||||
</Animated.View>
|
||||
) : status === 'completed' ? (
|
||||
<CheckCircle size={14} color={config.color} />
|
||||
) : (
|
||||
<XCircle size={14} color={config.color} />
|
||||
)}
|
||||
<Text style={[toolCallStylesheet.statusText, { color: config.color }]}>
|
||||
{config.label}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isExpanded && (
|
||||
<View style={toolCallStylesheet.expandedContent}>
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>
|
||||
Arguments
|
||||
</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
<Text style={toolCallStylesheet.sectionText}>
|
||||
{JSON.stringify(args, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{result !== undefined && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>
|
||||
Result
|
||||
</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
<Text style={toolCallStylesheet.sectionText}>
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{error !== undefined && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={[toolCallStylesheet.sectionTitle, toolCallStylesheet.errorSectionTitle]}>
|
||||
Error
|
||||
</Text>
|
||||
<View style={[toolCallStylesheet.sectionContent, toolCallStylesheet.errorSectionContent]}>
|
||||
<Text style={toolCallStylesheet.sectionText}>
|
||||
{JSON.stringify(error, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user