result-formatter.tsx•19.6 kB
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { CheckCircle, AlertCircle, FileText, Code, User, Calendar, GitBranch, Hash, ExternalLink, Star, GitFork, Eye, Clock, AlertTriangle, Zap, MessageSquare, GitCommit, GitPullRequest, Bug } from "lucide-react";
interface ResultFormatterProps {
result: any;
toolName: string;
}
export default function ResultFormatter({ result, toolName }: ResultFormatterProps) {
// Extract content from MCP response structure
const getContent = () => {
if (result?.content && Array.isArray(result.content)) {
return result.content;
}
return [{ text: JSON.stringify(result, null, 2) }];
};
const content = getContent();
// Helper function to parse JSON content
const parseContent = (text: string) => {
try {
return JSON.parse(text);
} catch {
return text;
}
};
// Helper function to format dates
const formatDate = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Helper function to truncate text
const truncateText = (text: string, maxLength: number = 100) => {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// Format arrays of data
const formatArray = (data: any[], type: string) => {
if (!Array.isArray(data) || data.length === 0) {
return (
<Card className="bg-muted/30">
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground text-center">No {type} found</p>
</CardContent>
</Card>
);
}
return (
<div className="space-y-3">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Badge variant="secondary">{data.length}</Badge>
{type.charAt(0).toUpperCase() + type.slice(1)}
</h3>
{data.map((item: any, index: number) => formatSingleItem(item, type, index))}
</div>
);
};
// Format individual items based on their type
const formatSingleItem = (data: any, type: string = '', index: number = 0) => {
// Repository
if (data.full_name && data.owner) {
return (
<Card key={index} className="bg-muted/30 hover:bg-muted/40 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-green-600 to-teal-600 rounded-lg flex items-center justify-center">
<GitBranch className="h-5 w-5 text-white" />
</div>
<div>
<CardTitle className="text-base">{data.name}</CardTitle>
<p className="text-sm text-muted-foreground">{data.full_name}</p>
</div>
</div>
<div className="flex gap-2">
{data.private && <Badge variant="destructive">Private</Badge>}
{data.fork && <Badge variant="outline">Fork</Badge>}
{data.archived && <Badge variant="secondary">Archived</Badge>}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{data.description && (
<p className="text-sm text-muted-foreground">{data.description}</p>
)}
<div className="flex flex-wrap gap-2">
{data.language && <Badge variant="default">{data.language}</Badge>}
{data.topics && data.topics.slice(0, 3).map((topic: string, i: number) => (
<Badge key={i} variant="outline" className="text-xs">{topic}</Badge>
))}
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 text-sm">
<div className="flex items-center gap-2">
<Star className="h-4 w-4 text-yellow-500" />
<span className="font-medium">{data.stargazers_count || 0}</span>
</div>
<div className="flex items-center gap-2">
<GitFork className="h-4 w-4 text-blue-500" />
<span className="font-medium">{data.forks_count || 0}</span>
</div>
<div className="flex items-center gap-2">
<Eye className="h-4 w-4 text-green-500" />
<span className="font-medium">{data.watchers_count || 0}</span>
</div>
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-red-500" />
<span className="font-medium">{data.open_issues_count || 0}</span>
</div>
</div>
{(data.updated_at || data.pushed_at) && (
<p className="text-xs text-muted-foreground">
Updated: {formatDate(data.updated_at || data.pushed_at)}
</p>
)}
{data.html_url && (
<a href={data.html_url} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:text-primary/80 text-sm">
<ExternalLink className="h-4 w-4" />
View Repository
</a>
)}
</CardContent>
</Card>
);
}
// User/Organization
if (data.login && (data.type === 'User' || data.type === 'Organization')) {
return (
<Card key={index} className="bg-muted/30 hover:bg-muted/40 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-r from-purple-600 to-blue-600 rounded-full flex items-center justify-center">
<User className="h-6 w-6 text-white" />
</div>
<div>
<CardTitle className="text-lg">{data.name || data.login}</CardTitle>
<p className="text-sm text-muted-foreground">@{data.login}</p>
{data.type && <Badge variant="outline" className="text-xs">{data.type}</Badge>}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{data.bio && <p className="text-sm text-muted-foreground">{data.bio}</p>}
<div className="grid grid-cols-2 gap-4 text-sm">
{data.public_repos !== undefined && (
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span>{data.public_repos} repositories</span>
</div>
)}
{data.followers !== undefined && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{data.followers} followers</span>
</div>
)}
{data.company && (
<div className="flex items-center gap-2">
<Hash className="h-4 w-4 text-muted-foreground" />
<span>{data.company}</span>
</div>
)}
{data.location && (
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span>{data.location}</span>
</div>
)}
</div>
{data.html_url && (
<a href={data.html_url} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:text-primary/80 text-sm">
<ExternalLink className="h-4 w-4" />
View Profile
</a>
)}
</CardContent>
</Card>
);
}
// Issue or Pull Request
if (data.number && (data.title || data.pull_request !== undefined)) {
const isPR = data.pull_request !== undefined;
return (
<Card key={index} className="bg-muted/30 hover:bg-muted/40 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${
isPR ? 'bg-gradient-to-r from-purple-600 to-blue-600' : 'bg-gradient-to-r from-green-600 to-teal-600'
}`}>
{isPR ? <GitPullRequest className="h-5 w-5 text-white" /> : <Bug className="h-5 w-5 text-white" />}
</div>
<div className="flex-1">
<CardTitle className="text-base">{truncateText(data.title, 80)}</CardTitle>
<p className="text-sm text-muted-foreground">#{data.number}</p>
</div>
</div>
<div className="flex gap-2">
<Badge variant={data.state === 'open' ? 'default' : data.state === 'closed' ? 'destructive' : 'secondary'}>
{data.state}
</Badge>
{isPR && <Badge variant="outline">PR</Badge>}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{data.body && (
<p className="text-sm text-muted-foreground">{truncateText(data.body, 150)}</p>
)}
<div className="flex flex-wrap gap-2">
{data.labels && data.labels.slice(0, 3).map((label: any, i: number) => (
<Badge key={i} variant="outline" className="text-xs" style={{backgroundColor: `#${label.color}20`}}>
{label.name}
</Badge>
))}
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
{data.user && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{data.user.login}</span>
</div>
)}
{data.created_at && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span>{formatDate(data.created_at)}</span>
</div>
)}
{data.comments !== undefined && (
<div className="flex items-center gap-2">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<span>{data.comments} comments</span>
</div>
)}
{data.assignee && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>@{data.assignee.login}</span>
</div>
)}
</div>
{data.html_url && (
<a href={data.html_url} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:text-primary/80 text-sm">
<ExternalLink className="h-4 w-4" />
View {isPR ? 'Pull Request' : 'Issue'}
</a>
)}
</CardContent>
</Card>
);
}
// Commit
if (data.sha && data.commit) {
return (
<Card key={index} className="bg-muted/30 hover:bg-muted/40 transition-colors">
<CardHeader className="pb-3">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-gradient-to-r from-orange-600 to-red-600 rounded-lg flex items-center justify-center">
<GitCommit className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<CardTitle className="text-base">{truncateText(data.commit.message, 80)}</CardTitle>
<p className="text-sm text-muted-foreground font-mono">{data.sha.substring(0, 7)}</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4 text-sm">
{data.commit.author && (
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-muted-foreground" />
<span>{data.commit.author.name}</span>
</div>
)}
{data.commit.author?.date && (
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span>{formatDate(data.commit.author.date)}</span>
</div>
)}
</div>
{data.stats && (
<div className="flex gap-4 text-sm">
<span className="text-green-600">+{data.stats.additions}</span>
<span className="text-red-600">-{data.stats.deletions}</span>
<span className="text-muted-foreground">{data.stats.total} changes</span>
</div>
)}
{data.html_url && (
<a href={data.html_url} target="_blank" rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:text-primary/80 text-sm">
<ExternalLink className="h-4 w-4" />
View Commit
</a>
)}
</CardContent>
</Card>
);
}
// Generic object formatting with better structure
if (typeof data === 'object' && data !== null) {
const entries = Object.entries(data).filter(([key, value]) =>
value !== null && value !== undefined && value !== ''
);
return (
<Card key={index} className="bg-muted/30">
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Code className="h-4 w-4" />
{type || 'Data'}
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
{entries.map(([key, value]) => (
<div key={key} className="flex justify-between items-start gap-4 py-1">
<span className="text-sm font-medium text-muted-foreground min-w-fit">
{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:
</span>
<span className="text-sm text-right flex-1">
{typeof value === 'boolean' ? (
<Badge variant={value ? 'default' : 'secondary'}>
{value ? 'Yes' : 'No'}
</Badge>
) : typeof value === 'object' ? (
<code className="text-xs bg-muted px-2 py-1 rounded">
{JSON.stringify(value, null, 2)}
</code>
) : key.includes('url') || key.includes('_url') ? (
<a href={String(value)} target="_blank" rel="noopener noreferrer"
className="text-primary hover:text-primary/80 text-xs">
<ExternalLink className="h-3 w-3 inline" />
</a>
) : key.includes('date') || key.includes('_at') ? (
formatDate(String(value))
) : (
truncateText(String(value), 50)
)}
</span>
</div>
))}
</CardContent>
</Card>
);
}
return null;
};
// Main formatting function
const formatGitHubContent = (data: any) => {
if (typeof data === 'string') {
const parsed = parseContent(data);
if (typeof parsed === 'object') {
data = parsed;
}
}
// Handle arrays (repositories, issues, commits, etc.)
if (Array.isArray(data)) {
// Determine the type based on array content
if (data.length > 0) {
const firstItem = data[0];
let type = 'items';
if (firstItem.full_name && firstItem.owner) type = 'repositories';
else if (firstItem.login) type = 'users';
else if (firstItem.number && firstItem.title) type = 'issues';
else if (firstItem.sha && firstItem.commit) type = 'commits';
else if (firstItem.name && firstItem.path) type = 'files';
return formatArray(data, type);
}
}
// Handle single objects
const formatted = formatSingleItem(data);
if (formatted) return formatted;
return null;
};
return (
<div className="space-y-4">
{content.map((item: any, index: number) => {
const text = item.text || item;
const parsedData = parseContent(text);
// Check if this is GitHub-related data
const isGitHubData = toolName.includes('github') || toolName.includes('repo') || toolName.includes('user');
if (isGitHubData && (typeof parsedData === 'object' || Array.isArray(parsedData))) {
const formatted = formatGitHubContent(parsedData);
if (formatted) {
return <div key={index}>{formatted}</div>;
}
}
// For text responses, format them nicely
if (typeof text === 'string') {
// Try to detect if it's structured data
if (text.includes('{') && text.includes('}')) {
try {
const jsonData = JSON.parse(text);
const formatted = formatGitHubContent(jsonData);
if (formatted) {
return <div key={index}>{formatted}</div>;
}
} catch {
// Fall through to plain text formatting
}
}
return (
<Card key={index} className="bg-muted/30">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-gradient-to-r from-blue-600 to-cyan-600 rounded-lg flex items-center justify-center flex-shrink-0">
<FileText className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<pre className="text-sm whitespace-pre-wrap text-foreground font-mono bg-muted/50 p-3 rounded-md overflow-x-auto">
{text}
</pre>
</div>
</div>
</CardContent>
</Card>
);
}
// Generic object display with better formatting
return (
<Card key={index} className="bg-muted/30">
<CardContent className="pt-6">
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-gradient-to-r from-purple-600 to-blue-600 rounded-lg flex items-center justify-center flex-shrink-0">
<Code className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<pre className="text-sm whitespace-pre-wrap overflow-x-auto text-foreground font-mono bg-muted/50 p-3 rounded-md">
{JSON.stringify(parsedData, null, 2)}
</pre>
</div>
</div>
</CardContent>
</Card>
);
})}
</div>
);
}