mirror of https://github.com/kortix-ai/suna.git
feat: add tool views and fixes
This commit is contained in:
parent
3746dc25c4
commit
94d3229e51
|
@ -1,5 +1,6 @@
|
|||
import { Message } from '@/api/chat-api';
|
||||
import { useThemedStyles } from '@/hooks/useThemeColor';
|
||||
import { useSelectedProject } from '@/stores/ui-store';
|
||||
import React, { useRef } from 'react';
|
||||
import { Dimensions, View } from 'react-native';
|
||||
import { DrawerLayout } from 'react-native-gesture-handler';
|
||||
|
@ -29,6 +30,7 @@ export const PanelContainer: React.FC<PanelContainerProps> = ({
|
|||
}) => {
|
||||
const leftDrawerRef = useRef<DrawerLayout>(null);
|
||||
const rightDrawerRef = useRef<DrawerLayout>(null);
|
||||
const selectedProject = useSelectedProject();
|
||||
|
||||
const styles = useThemedStyles((theme) => ({
|
||||
container: {
|
||||
|
@ -65,7 +67,12 @@ export const PanelContainer: React.FC<PanelContainerProps> = ({
|
|||
);
|
||||
|
||||
const rightDrawerContent = (
|
||||
<RightPanel isVisible={true} onClose={onCloseRight} messages={messages} />
|
||||
<RightPanel
|
||||
isVisible={true}
|
||||
onClose={onCloseRight}
|
||||
messages={messages}
|
||||
sandboxId={selectedProject?.sandbox?.id}
|
||||
/>
|
||||
);
|
||||
|
||||
const mainContent = (
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
useToolViewState,
|
||||
useUpdateToolSnapshots
|
||||
} from '@/stores/ui-store';
|
||||
import { GenericToolView } from './ToolViews/GenericToolView';
|
||||
import { ToolView } from './ToolViews/ToolViewRegistry';
|
||||
import { Body, Caption, H4 } from './Typography';
|
||||
|
||||
const { width: SCREEN_WIDTH } = Dimensions.get('window');
|
||||
|
@ -24,9 +24,10 @@ interface RightPanelProps {
|
|||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
messages?: Message[];
|
||||
sandboxId?: string;
|
||||
}
|
||||
|
||||
export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, messages = [] }) => {
|
||||
export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, messages = [], sandboxId }) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const toolViewState = useToolViewState();
|
||||
|
@ -283,7 +284,7 @@ export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, mess
|
|||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Computer size={16} color={styles.title.color} />
|
||||
<H4 style={styles.title}>Suna's Computer</H4>
|
||||
<H4 style={styles.title}>Suna's Computer</H4>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
|
||||
<X size={16} color={styles.title.color} />
|
||||
|
@ -296,7 +297,7 @@ export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, mess
|
|||
</View>
|
||||
<Body style={styles.emptyTitle}>No tool activity</Body>
|
||||
<Body style={styles.emptySubtitle}>
|
||||
Tool calls and computer interactions will appear here when they're being executed.
|
||||
Tool calls and computer interactions will appear here when they're being executed.
|
||||
</Body>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -307,8 +308,7 @@ export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, mess
|
|||
<View style={styles.panel}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Computer size={16} color={styles.title.color} />
|
||||
<H4 style={styles.title}>Suna's Computer</H4>
|
||||
<H4 style={styles.title}>Suna's Computer</H4>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerActions}>
|
||||
|
@ -330,10 +330,13 @@ export const RightPanel: React.FC<RightPanelProps> = ({ isVisible, onClose, mess
|
|||
showsVerticalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
<GenericToolView
|
||||
<ToolView
|
||||
name={currentSnapshot?.toolCall?.functionName}
|
||||
toolCall={currentSnapshot?.toolCall}
|
||||
isStreaming={isGenerating}
|
||||
isSuccess={true}
|
||||
sandboxId={sandboxId}
|
||||
messages={messages}
|
||||
/>
|
||||
</ScrollView>
|
||||
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import { useTheme } from '@/hooks/useThemeColor';
|
||||
import { MessageSquare, Paperclip } from 'lucide-react-native';
|
||||
import React from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { FileAttachment } from '../FileAttachment';
|
||||
import { Body, Caption } from '../Typography';
|
||||
|
||||
export interface AskToolViewProps {
|
||||
toolCall?: any;
|
||||
isStreaming?: boolean;
|
||||
isSuccess?: boolean;
|
||||
onFilePress?: (filePath: string) => void;
|
||||
sandboxId?: string;
|
||||
}
|
||||
|
||||
const extractAskData = (toolCall: any) => {
|
||||
// Handle attachments - can be string, array, or object
|
||||
let attachments: string[] = [];
|
||||
|
||||
const rawAttachments = toolCall?.parameters?.attachments ||
|
||||
toolCall?.input?.attachments ||
|
||||
toolCall?.arguments?.attachments ||
|
||||
toolCall?.parameters?.files ||
|
||||
toolCall?.input?.files ||
|
||||
toolCall?.arguments?.files;
|
||||
|
||||
if (Array.isArray(rawAttachments)) {
|
||||
// Handle array case - flat map to handle nested arrays and split comma-separated strings
|
||||
attachments = rawAttachments.flatMap((item: any) => {
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
// Split comma-separated strings
|
||||
return item.split(',').map(file => file.trim()).filter(file => file.length > 0);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
} else if (typeof rawAttachments === 'string' && rawAttachments.trim()) {
|
||||
// Handle single string case - split by comma in case multiple files are provided
|
||||
attachments = rawAttachments.split(',').map(file => file.trim()).filter(file => file.length > 0);
|
||||
} else if (rawAttachments && typeof rawAttachments === 'object') {
|
||||
// Handle object case - extract values and process them
|
||||
const values = Object.values(rawAttachments).filter(Boolean);
|
||||
attachments = values.flatMap((item: any) => {
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
// Split comma-separated strings
|
||||
return item.split(',').map(file => file.trim()).filter(file => file.length > 0);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
// Remove duplicates and empty strings
|
||||
attachments = [...new Set(attachments)].filter(attachment => attachment.length > 0);
|
||||
|
||||
const text = toolCall?.parameters?.text || toolCall?.input?.text || toolCall?.arguments?.text || '';
|
||||
|
||||
return {
|
||||
text,
|
||||
attachments,
|
||||
};
|
||||
};
|
||||
|
||||
export const AskToolView: React.FC<AskToolViewProps> = ({
|
||||
toolCall,
|
||||
isStreaming = false,
|
||||
isSuccess = true,
|
||||
onFilePress,
|
||||
sandboxId,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
section: {
|
||||
padding: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: theme.foreground,
|
||||
marginBottom: 12,
|
||||
fontWeight: '600' as const,
|
||||
},
|
||||
attachmentsList: {
|
||||
gap: 12,
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
emptyIcon: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: theme.muted + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: theme.foreground,
|
||||
textAlign: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptySubtitle: {
|
||||
color: theme.mutedForeground,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.border,
|
||||
backgroundColor: theme.muted + '10',
|
||||
},
|
||||
footerBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.border,
|
||||
},
|
||||
footerText: {
|
||||
color: theme.mutedForeground,
|
||||
fontSize: 12,
|
||||
},
|
||||
});
|
||||
|
||||
if (!toolCall) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.emptyState}>
|
||||
<View style={styles.emptyIcon}>
|
||||
<MessageSquare size={32} color={theme.mutedForeground} />
|
||||
</View>
|
||||
<Body style={styles.emptyTitle}>No question selected</Body>
|
||||
<Body style={styles.emptySubtitle}>
|
||||
Select a question to view its details
|
||||
</Body>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const { attachments } = extractAskData(toolCall);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView style={styles.content}>
|
||||
{Array.isArray(attachments) && attachments.length > 0 ? (
|
||||
<View style={styles.section}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 12 }}>
|
||||
<Paperclip size={16} color={theme.mutedForeground} />
|
||||
<Caption style={[styles.sectionTitle, { marginLeft: 8, marginBottom: 0 }]}>
|
||||
Files ({attachments.length})
|
||||
</Caption>
|
||||
</View>
|
||||
|
||||
<View style={styles.attachmentsList}>
|
||||
{attachments.map((attachment: string, index: number) => (
|
||||
<FileAttachment
|
||||
key={index}
|
||||
filepath={attachment}
|
||||
sandboxId={sandboxId}
|
||||
onPress={onFilePress}
|
||||
showPreview={true}
|
||||
layout="grid"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.emptyState}>
|
||||
<View style={styles.emptyIcon}>
|
||||
<MessageSquare size={32} color={theme.mutedForeground} />
|
||||
</View>
|
||||
<Body style={styles.emptyTitle}>Question Asked</Body>
|
||||
<Body style={styles.emptySubtitle}>
|
||||
No files attached to this question
|
||||
</Body>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,369 @@
|
|||
import { useTheme } from '@/hooks/useThemeColor';
|
||||
import { CheckCircle, ListChecks, Paperclip, Sparkles, Trophy } from 'lucide-react-native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming } from 'react-native-reanimated';
|
||||
import { MarkdownComponent } from '../../utils/markdown-renderer';
|
||||
import { FileAttachment } from '../FileAttachment';
|
||||
import { Body, Caption } from '../Typography';
|
||||
import { ToolViewProps } from './ToolViewRegistry';
|
||||
|
||||
export interface CompleteToolViewProps extends ToolViewProps {
|
||||
messages?: any[]; // Add messages prop to access previous message
|
||||
}
|
||||
|
||||
interface CompleteContent {
|
||||
summary?: string;
|
||||
result?: string;
|
||||
tasksCompleted?: string[];
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
const extractCompleteData = (toolCall: any, messages?: any[]): CompleteContent => {
|
||||
// For complete tool, data comes from the PREVIOUS message in the conversation
|
||||
// The complete tool is a termination signal that refers to the previous message
|
||||
let summary = '';
|
||||
let attachments: string[] = [];
|
||||
let tasksCompleted: string[] = [];
|
||||
|
||||
if (messages && toolCall) {
|
||||
// Find the message that contains this tool call
|
||||
const currentMessageIndex = messages.findIndex(msg =>
|
||||
msg.message_id === toolCall.messageId ||
|
||||
(msg.content && msg.content.includes && msg.content.includes(toolCall.functionName))
|
||||
);
|
||||
|
||||
if (currentMessageIndex > 0) {
|
||||
// Get the previous message (which contains the actual completion content)
|
||||
const previousMessage = messages[currentMessageIndex - 1];
|
||||
|
||||
if (previousMessage && previousMessage.type === 'assistant') {
|
||||
// Extract content from previous message
|
||||
summary = previousMessage.content || '';
|
||||
|
||||
// Clean up function calls from the content
|
||||
if (typeof summary === 'string') {
|
||||
summary = summary
|
||||
.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, '')
|
||||
.replace(/<invoke name="complete"[\s\S]*?<\/invoke>/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Parse attachments from previous message if mentioned
|
||||
const attachmentMatches = summary.match(/attachments?[=:]\s*["']([^"']+)["']/gi);
|
||||
if (attachmentMatches) {
|
||||
attachments = attachmentMatches.flatMap(match => {
|
||||
const extracted = match.match(/["']([^"']+)["']/);
|
||||
return extracted ? extracted[1].split(',').map(f => f.trim()) : [];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to tool parameters if no previous message found
|
||||
if (!summary) {
|
||||
summary = toolCall?.parameters?.summary ||
|
||||
toolCall?.parameters?.text ||
|
||||
toolCall?.input?.summary ||
|
||||
toolCall?.input?.text ||
|
||||
toolCall?.parameters?.message ||
|
||||
toolCall?.input?.message || '';
|
||||
}
|
||||
|
||||
// Parse tasks from summary if it contains bullet points
|
||||
if (summary) {
|
||||
const taskMatches = summary.match(/[•\-\*]\s*([^\n]+)/g);
|
||||
if (taskMatches) {
|
||||
tasksCompleted = taskMatches.map((task: string) =>
|
||||
task.replace(/^[•\-\*]\s*/, '').trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
attachments = [...new Set(attachments)].filter(attachment => attachment.length > 0);
|
||||
|
||||
return {
|
||||
summary: summary || '',
|
||||
attachments,
|
||||
tasksCompleted,
|
||||
};
|
||||
};
|
||||
|
||||
export const CompleteToolView: React.FC<CompleteToolViewProps> = ({
|
||||
toolCall,
|
||||
messages,
|
||||
isStreaming = false,
|
||||
isSuccess = true,
|
||||
onFilePress,
|
||||
sandboxId,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const [progress] = useState(isStreaming ? 0 : 100);
|
||||
|
||||
// Success animation
|
||||
const sparkleOpacity = useSharedValue(0);
|
||||
const sparkleScale = useSharedValue(0.8);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStreaming && isSuccess) {
|
||||
sparkleOpacity.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 600 }),
|
||||
withTiming(0, { duration: 600 })
|
||||
),
|
||||
3,
|
||||
false
|
||||
);
|
||||
sparkleScale.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(1.2, { duration: 600 }),
|
||||
withTiming(0.8, { duration: 600 })
|
||||
),
|
||||
3,
|
||||
false
|
||||
);
|
||||
}
|
||||
}, [isStreaming, isSuccess]);
|
||||
|
||||
const sparkleAnimatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: sparkleOpacity.value,
|
||||
transform: [{ scale: sparkleScale.value }],
|
||||
}));
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
section: {
|
||||
padding: 16,
|
||||
},
|
||||
successContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 32,
|
||||
},
|
||||
successIcon: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: theme.primary + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
position: 'relative',
|
||||
},
|
||||
sparkleIcon: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
},
|
||||
summaryContainer: {
|
||||
backgroundColor: theme.muted + '20',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
color: theme.mutedForeground,
|
||||
fontWeight: '600',
|
||||
marginLeft: 8,
|
||||
},
|
||||
taskItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: theme.muted + '20',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
taskIcon: {
|
||||
marginRight: 12,
|
||||
marginTop: 2,
|
||||
},
|
||||
taskContent: {
|
||||
flex: 1,
|
||||
},
|
||||
attachmentsList: {
|
||||
gap: 12,
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
emptyIcon: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 32,
|
||||
backgroundColor: theme.muted + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyTitle: {
|
||||
color: theme.foreground,
|
||||
textAlign: 'center',
|
||||
marginBottom: 8,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptySubtitle: {
|
||||
color: theme.mutedForeground,
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
},
|
||||
progressContainer: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
progressHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
progressText: {
|
||||
color: theme.mutedForeground,
|
||||
fontSize: 14,
|
||||
},
|
||||
progressBar: {
|
||||
height: 4,
|
||||
backgroundColor: theme.muted + '40',
|
||||
borderRadius: 2,
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
backgroundColor: theme.primary,
|
||||
borderRadius: 2,
|
||||
},
|
||||
});
|
||||
|
||||
if (!toolCall) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.emptyState}>
|
||||
<View style={styles.emptyIcon}>
|
||||
<CheckCircle size={32} color={theme.mutedForeground} />
|
||||
</View>
|
||||
<Body style={styles.emptyTitle}>No task selected</Body>
|
||||
<Body style={styles.emptySubtitle}>
|
||||
Select a completed task to view its details
|
||||
</Body>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, attachments, tasksCompleted } = extractCompleteData(toolCall, messages);
|
||||
const hasContent = summary || (attachments && attachments.length > 0) || (tasksCompleted && tasksCompleted.length > 0);
|
||||
|
||||
// Don't render if no meaningful content (prevents empty duplicate rendering)
|
||||
if (!hasContent && !isStreaming) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.successContainer}>
|
||||
<View style={styles.successIcon}>
|
||||
<Trophy size={40} color={theme.primary} />
|
||||
<Animated.View style={[styles.sparkleIcon, sparkleAnimatedStyle]}>
|
||||
<Sparkles size={20} color="#FFD700" />
|
||||
</Animated.View>
|
||||
</View>
|
||||
<Body style={styles.emptyTitle}>Task Completed!</Body>
|
||||
<Body style={styles.emptySubtitle}>
|
||||
Successfully finished the assigned task
|
||||
</Body>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView style={styles.content}>
|
||||
<View style={styles.section}>
|
||||
{/* Summary Section with Markdown */}
|
||||
{summary && (
|
||||
<View style={styles.summaryContainer}>
|
||||
<MarkdownComponent>
|
||||
{summary}
|
||||
</MarkdownComponent>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Tasks Completed Section with Markdown */}
|
||||
{tasksCompleted && tasksCompleted.length > 0 && (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<ListChecks size={16} color={theme.mutedForeground} />
|
||||
<Caption style={styles.sectionTitle}>
|
||||
Tasks Completed
|
||||
</Caption>
|
||||
</View>
|
||||
{tasksCompleted.map((task, index) => (
|
||||
<View key={index} style={styles.taskItem}>
|
||||
<CheckCircle size={16} color={theme.primary} style={styles.taskIcon} />
|
||||
<View style={styles.taskContent}>
|
||||
<MarkdownComponent>
|
||||
{task}
|
||||
</MarkdownComponent>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Attachments Section */}
|
||||
{attachments && attachments.length > 0 && (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Paperclip size={16} color={theme.mutedForeground} />
|
||||
<Caption style={styles.sectionTitle}>
|
||||
Files ({attachments.length})
|
||||
</Caption>
|
||||
</View>
|
||||
<View style={styles.attachmentsList}>
|
||||
{attachments.map((attachment, index) => (
|
||||
<FileAttachment
|
||||
key={index}
|
||||
filepath={attachment}
|
||||
sandboxId={sandboxId}
|
||||
onPress={onFilePress}
|
||||
showPreview={true}
|
||||
layout="grid"
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Progress Section for Streaming */}
|
||||
{isStreaming && (
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressHeader}>
|
||||
<Caption style={styles.progressText}>
|
||||
Completing task...
|
||||
</Caption>
|
||||
<Caption style={styles.progressText}>
|
||||
{progress}%
|
||||
</Caption>
|
||||
</View>
|
||||
<View style={styles.progressBar}>
|
||||
<View style={[styles.progressFill, { width: `${progress}%` }]} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,123 @@
|
|||
import { useTheme } from '@/hooks/useThemeColor';
|
||||
import { AlertTriangle, CheckCircle, CheckCircle2, Clock, Computer, MessageCircleQuestion } from 'lucide-react-native';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Caption, H4 } from '../Typography';
|
||||
|
||||
interface ToolHeaderProps {
|
||||
toolName: string;
|
||||
isStreaming?: boolean;
|
||||
isSuccess?: boolean;
|
||||
icon?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
const getToolIcon = (toolName: string) => {
|
||||
switch (toolName) {
|
||||
case 'ask':
|
||||
return MessageCircleQuestion;
|
||||
case 'complete':
|
||||
return CheckCircle2;
|
||||
default:
|
||||
return Computer;
|
||||
}
|
||||
};
|
||||
|
||||
const getToolDisplayName = (toolName: string) => {
|
||||
switch (toolName) {
|
||||
case 'ask':
|
||||
return 'Ask User';
|
||||
case 'complete':
|
||||
return 'Task Complete';
|
||||
default:
|
||||
return toolName?.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()) || 'Unknown Tool';
|
||||
}
|
||||
};
|
||||
|
||||
export const ToolHeader: React.FC<ToolHeaderProps> = ({
|
||||
toolName,
|
||||
isStreaming = false,
|
||||
isSuccess = true,
|
||||
icon: IconComponent
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const Icon = IconComponent || getToolIcon(toolName);
|
||||
const displayName = getToolDisplayName(toolName);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
backgroundColor: theme.muted + '20',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.border,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: theme.primary + '20',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
title: {
|
||||
color: theme.foreground,
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600' as const,
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
|
||||
const statusColor = isStreaming
|
||||
? theme.accent
|
||||
: isSuccess
|
||||
? theme.primary
|
||||
: theme.destructive;
|
||||
|
||||
const statusText = isStreaming
|
||||
? 'Running...'
|
||||
: isSuccess
|
||||
? 'Success'
|
||||
: 'Failed';
|
||||
|
||||
const StatusIcon = isStreaming
|
||||
? Clock
|
||||
: isSuccess
|
||||
? CheckCircle
|
||||
: AlertTriangle;
|
||||
|
||||
return (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Icon size={16} color={theme.primary} />
|
||||
</View>
|
||||
<H4 style={styles.title}>{displayName}</H4>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statusBadge, { backgroundColor: statusColor + '20' }]}>
|
||||
<StatusIcon size={12} color={statusColor} />
|
||||
<Caption style={[styles.statusText, { color: statusColor }]}>
|
||||
{statusText}
|
||||
</Caption>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { AskToolView } from './AskToolView';
|
||||
import { CompleteToolView } from './CompleteToolView';
|
||||
import { GenericToolView } from './GenericToolView';
|
||||
import { ToolHeader } from './ToolHeader';
|
||||
|
||||
export interface ToolViewProps {
|
||||
name?: string;
|
||||
toolCall?: any;
|
||||
isStreaming?: boolean;
|
||||
isSuccess?: boolean;
|
||||
onFilePress?: (filePath: string) => void;
|
||||
sandboxId?: string;
|
||||
messages?: any[]; // Add messages prop for complete tool
|
||||
// Future props can be added here
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type ToolViewComponent = React.ComponentType<ToolViewProps>;
|
||||
|
||||
const defaultRegistry: Record<string, ToolViewComponent> = {
|
||||
'ask': AskToolView,
|
||||
'complete': CompleteToolView,
|
||||
'default': GenericToolView,
|
||||
};
|
||||
|
||||
class ToolViewRegistry {
|
||||
private registry: Record<string, ToolViewComponent>;
|
||||
|
||||
constructor(initialRegistry: Record<string, ToolViewComponent> = {}) {
|
||||
this.registry = { ...defaultRegistry, ...initialRegistry };
|
||||
}
|
||||
|
||||
register(toolName: string, component: ToolViewComponent): void {
|
||||
this.registry[toolName] = component;
|
||||
}
|
||||
|
||||
get(toolName: string): ToolViewComponent {
|
||||
return this.registry[toolName] || this.registry['default'];
|
||||
}
|
||||
|
||||
has(toolName: string): boolean {
|
||||
return toolName in this.registry;
|
||||
}
|
||||
}
|
||||
|
||||
export const toolViewRegistry = new ToolViewRegistry();
|
||||
|
||||
export function ToolView({ name = 'default', ...props }: ToolViewProps) {
|
||||
const ToolViewComponent = toolViewRegistry.get(name);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<ToolHeader
|
||||
toolName={name}
|
||||
isStreaming={props.isStreaming}
|
||||
isSuccess={props.isSuccess}
|
||||
/>
|
||||
<ToolViewComponent name={name} {...props} />
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ import { ParsedToolCall } from '@/utils/message-parser';
|
|||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Body, Caption } from '../Typography';
|
||||
import { AskToolView } from './AskToolView';
|
||||
|
||||
// Generic tool view for unknown tools
|
||||
export const GenericToolView: React.FC<{ toolCall: ParsedToolCall }> = ({ toolCall }) => {
|
||||
|
@ -197,6 +198,11 @@ export const WebSearchToolView: React.FC<{ toolCall: ParsedToolCall }> = ({ tool
|
|||
);
|
||||
};
|
||||
|
||||
// Ask tool view wrapper to match the registry interface
|
||||
const AskToolViewWrapper: React.FC<{ toolCall: ParsedToolCall }> = ({ toolCall }) => {
|
||||
return <AskToolView toolCall={toolCall} />;
|
||||
};
|
||||
|
||||
// Tool registry - maps tool names to their view components
|
||||
export const TOOL_VIEW_REGISTRY: Record<string, React.FC<{ toolCall: ParsedToolCall }>> = {
|
||||
'create-file': FileToolView,
|
||||
|
@ -206,6 +212,7 @@ export const TOOL_VIEW_REGISTRY: Record<string, React.FC<{ toolCall: ParsedToolC
|
|||
'execute-command': CommandToolView,
|
||||
'web-search': WebSearchToolView,
|
||||
'crawl-webpage': WebSearchToolView,
|
||||
'ask': AskToolViewWrapper,
|
||||
};
|
||||
|
||||
// Main tool view component that uses the registry
|
||||
|
|
Loading…
Reference in New Issue