2025-05-25 23:33:50 +08:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
import {
|
|
|
|
CheckCircle2,
|
|
|
|
CheckCircle,
|
|
|
|
AlertTriangle,
|
|
|
|
Loader2,
|
|
|
|
ListChecks,
|
|
|
|
Sparkles,
|
|
|
|
Trophy,
|
|
|
|
Paperclip,
|
|
|
|
ExternalLink,
|
|
|
|
} from 'lucide-react';
|
|
|
|
import { ToolViewProps } from './types';
|
|
|
|
import {
|
|
|
|
formatTimestamp,
|
|
|
|
getToolTitle,
|
|
|
|
normalizeContentToString,
|
2025-05-28 20:07:54 +08:00
|
|
|
extractToolData,
|
2025-05-25 23:33:50 +08:00
|
|
|
getFileIconAndColor,
|
|
|
|
} from './utils';
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
import { Progress } from '@/components/ui/progress';
|
|
|
|
import { Markdown } from '@/components/ui/markdown';
|
|
|
|
|
|
|
|
interface CompleteContent {
|
|
|
|
summary?: string;
|
|
|
|
result?: string;
|
|
|
|
tasksCompleted?: string[];
|
|
|
|
finalOutput?: string;
|
|
|
|
attachments?: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CompleteToolViewProps extends ToolViewProps {
|
|
|
|
onFileClick?: (filePath: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function CompleteToolView({
|
|
|
|
name = 'complete',
|
|
|
|
assistantContent,
|
|
|
|
toolContent,
|
|
|
|
assistantTimestamp,
|
|
|
|
toolTimestamp,
|
|
|
|
isSuccess = true,
|
|
|
|
isStreaming = false,
|
|
|
|
onFileClick,
|
|
|
|
}: CompleteToolViewProps) {
|
|
|
|
const [completeData, setCompleteData] = useState<CompleteContent>({});
|
|
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (assistantContent) {
|
|
|
|
try {
|
|
|
|
const contentStr = normalizeContentToString(assistantContent);
|
2025-05-28 04:12:19 +08:00
|
|
|
|
2025-06-06 18:19:47 +08:00
|
|
|
let cleanContent = contentStr
|
|
|
|
.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, '')
|
|
|
|
.replace(/<invoke name="complete"[\s\S]*?<\/invoke>/g, '')
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
const completeMatch = cleanContent.match(/<complete[^>]*>([^<]*)<\/complete>/);
|
2025-05-25 23:33:50 +08:00
|
|
|
if (completeMatch) {
|
|
|
|
setCompleteData(prev => ({ ...prev, summary: completeMatch[1].trim() }));
|
2025-06-06 18:19:47 +08:00
|
|
|
} else if (cleanContent) {
|
|
|
|
setCompleteData(prev => ({ ...prev, summary: cleanContent }));
|
2025-05-25 23:33:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const attachmentsMatch = contentStr.match(/attachments=["']([^"']*)["']/i);
|
|
|
|
if (attachmentsMatch) {
|
|
|
|
const attachments = attachmentsMatch[1].split(',').map(a => a.trim()).filter(a => a.length > 0);
|
|
|
|
setCompleteData(prev => ({ ...prev, attachments }));
|
|
|
|
}
|
|
|
|
|
2025-06-06 18:19:47 +08:00
|
|
|
const taskMatches = cleanContent.match(/- ([^\n]+)/g);
|
2025-05-25 23:33:50 +08:00
|
|
|
if (taskMatches) {
|
|
|
|
const tasks = taskMatches.map(task => task.replace('- ', '').trim());
|
|
|
|
setCompleteData(prev => ({ ...prev, tasksCompleted: tasks }));
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Error parsing complete content:', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [assistantContent]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (toolContent && !isStreaming) {
|
|
|
|
try {
|
|
|
|
const contentStr = normalizeContentToString(toolContent);
|
|
|
|
const toolResultMatch = contentStr.match(/ToolResult\([^)]*output=['"]([^'"]+)['"]/);
|
|
|
|
if (toolResultMatch) {
|
|
|
|
setCompleteData(prev => ({ ...prev, result: toolResultMatch[1] }));
|
|
|
|
} else {
|
|
|
|
setCompleteData(prev => ({ ...prev, result: contentStr }));
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.error('Error parsing tool response:', e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [toolContent, isStreaming]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (isStreaming) {
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
setProgress((prevProgress) => {
|
|
|
|
if (prevProgress >= 95) {
|
|
|
|
clearInterval(timer);
|
|
|
|
return prevProgress;
|
|
|
|
}
|
|
|
|
return prevProgress + 5;
|
|
|
|
});
|
|
|
|
}, 300);
|
|
|
|
return () => clearInterval(timer);
|
|
|
|
} else {
|
|
|
|
setProgress(100);
|
|
|
|
}
|
|
|
|
}, [isStreaming]);
|
|
|
|
|
|
|
|
const toolTitle = getToolTitle(name) || 'Task Complete';
|
|
|
|
|
|
|
|
const handleFileClick = (filePath: string) => {
|
|
|
|
if (onFileClick) {
|
|
|
|
onFileClick(filePath);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Card className="gap-0 flex border shadow-none border-t border-b-0 border-x-0 p-0 rounded-none flex-col h-full overflow-hidden bg-white dark:bg-zinc-950">
|
|
|
|
<CardHeader className="h-14 bg-zinc-50/80 dark:bg-zinc-900/80 backdrop-blur-sm border-b p-2 px-4 space-y-2">
|
|
|
|
<div className="flex flex-row items-center justify-between">
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
<div className="relative p-2 rounded-lg bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 border border-emerald-500/20">
|
|
|
|
<CheckCircle2 className="w-5 h-5 text-emerald-500 dark:text-emerald-400" />
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<CardTitle className="text-base font-medium text-zinc-900 dark:text-zinc-100">
|
|
|
|
{toolTitle}
|
|
|
|
</CardTitle>
|
|
|
|
</div>
|
|
|
|
</div>
|
2025-05-28 04:12:19 +08:00
|
|
|
|
2025-05-25 23:33:50 +08:00
|
|
|
{!isStreaming && (
|
2025-05-28 04:12:19 +08:00
|
|
|
<Badge
|
|
|
|
variant="secondary"
|
2025-05-25 23:33:50 +08:00
|
|
|
className={
|
2025-05-28 04:12:19 +08:00
|
|
|
isSuccess
|
|
|
|
? "bg-gradient-to-b from-emerald-200 to-emerald-100 text-emerald-700 dark:from-emerald-800/50 dark:to-emerald-900/60 dark:text-emerald-300"
|
2025-05-25 23:33:50 +08:00
|
|
|
: "bg-gradient-to-b from-rose-200 to-rose-100 text-rose-700 dark:from-rose-800/50 dark:to-rose-900/60 dark:text-rose-300"
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{isSuccess ? (
|
|
|
|
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
|
|
|
) : (
|
|
|
|
<AlertTriangle className="h-3.5 w-3.5 mr-1" />
|
|
|
|
)}
|
|
|
|
{isSuccess ? 'Completed' : 'Failed'}
|
|
|
|
</Badge>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{isStreaming && (
|
|
|
|
<Badge className="bg-gradient-to-b from-blue-200 to-blue-100 text-blue-700 dark:from-blue-800/50 dark:to-blue-900/60 dark:text-blue-300">
|
|
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1" />
|
|
|
|
Completing
|
|
|
|
</Badge>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</CardHeader>
|
|
|
|
|
|
|
|
<CardContent className="p-0 flex-1 overflow-hidden relative">
|
|
|
|
<ScrollArea className="h-full w-full">
|
|
|
|
<div className="p-4 space-y-6">
|
|
|
|
{/* Success Animation/Icon - Only show when completed successfully */}
|
|
|
|
{!isStreaming && isSuccess && !completeData.summary && !completeData.tasksCompleted && !completeData.attachments && (
|
|
|
|
<div className="flex justify-center">
|
|
|
|
<div className="relative">
|
|
|
|
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-emerald-100 to-emerald-200 dark:from-emerald-800/40 dark:to-emerald-900/60 flex items-center justify-center">
|
|
|
|
<Trophy className="h-10 w-10 text-emerald-600 dark:text-emerald-400" />
|
|
|
|
</div>
|
|
|
|
<div className="absolute -top-1 -right-1">
|
|
|
|
<Sparkles className="h-5 w-5 text-yellow-500 animate-pulse" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Summary Section */}
|
|
|
|
{completeData.summary && (
|
|
|
|
<div className="space-y-2">
|
|
|
|
<div className="bg-muted/50 rounded-lg p-4 border border-border">
|
|
|
|
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">
|
|
|
|
{completeData.summary}
|
|
|
|
</Markdown>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Attachments Section */}
|
|
|
|
{completeData.attachments && completeData.attachments.length > 0 && (
|
|
|
|
<div className="space-y-3">
|
|
|
|
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
|
|
<Paperclip className="h-4 w-4" />
|
|
|
|
Files ({completeData.attachments.length})
|
|
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-2">
|
|
|
|
{completeData.attachments.map((attachment, index) => {
|
|
|
|
const { icon: FileIcon, color, bgColor } = getFileIconAndColor(attachment);
|
|
|
|
const fileName = attachment.split('/').pop() || attachment;
|
|
|
|
const filePath = attachment.includes('/') ? attachment.substring(0, attachment.lastIndexOf('/')) : '';
|
2025-05-28 04:12:19 +08:00
|
|
|
|
2025-05-25 23:33:50 +08:00
|
|
|
return (
|
|
|
|
<button
|
|
|
|
key={index}
|
|
|
|
onClick={() => handleFileClick(attachment)}
|
|
|
|
className="flex items-center gap-3 p-3 bg-muted/30 rounded-lg border border-border/50 hover:bg-muted/50 transition-colors group cursor-pointer text-left"
|
|
|
|
>
|
|
|
|
<div className="flex-shrink-0">
|
|
|
|
<div className={cn(
|
|
|
|
"w-10 h-10 rounded-lg bg-gradient-to-br flex items-center justify-center",
|
|
|
|
bgColor
|
|
|
|
)}>
|
|
|
|
<FileIcon className={cn("h-5 w-5", color)} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
<p className="text-sm font-medium text-foreground truncate">
|
|
|
|
{fileName}
|
|
|
|
</p>
|
|
|
|
{filePath && (
|
|
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
|
|
{filePath}
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
<div className="flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
<ExternalLink className="h-4 w-4 text-muted-foreground" />
|
|
|
|
</div>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Tasks Completed Section */}
|
|
|
|
{completeData.tasksCompleted && completeData.tasksCompleted.length > 0 && (
|
|
|
|
<div className="space-y-3">
|
|
|
|
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
|
|
|
<ListChecks className="h-4 w-4" />
|
|
|
|
Tasks Completed
|
|
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
|
|
{completeData.tasksCompleted.map((task, index) => (
|
2025-05-28 04:12:19 +08:00
|
|
|
<div
|
2025-05-25 23:33:50 +08:00
|
|
|
key={index}
|
|
|
|
className="flex items-start gap-3 p-3 bg-muted/30 rounded-lg border border-border/50"
|
|
|
|
>
|
|
|
|
<div className="mt-1 flex-shrink-0">
|
|
|
|
<CheckCircle className="h-4 w-4 text-emerald-500" />
|
|
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 [&>:last-child]:mb-0">
|
|
|
|
{task}
|
|
|
|
</Markdown>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Progress Section for Streaming */}
|
|
|
|
{isStreaming && (
|
|
|
|
<div className="space-y-3">
|
|
|
|
<div className="flex items-center justify-between text-sm">
|
|
|
|
<span className="text-muted-foreground">
|
|
|
|
Completing task...
|
|
|
|
</span>
|
|
|
|
<span className="text-muted-foreground text-xs">
|
|
|
|
{progress}%
|
|
|
|
</span>
|
|
|
|
</div>
|
2025-05-27 15:10:50 +08:00
|
|
|
<Progress value={progress} className="h-1" />
|
2025-05-25 23:33:50 +08:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{/* Empty State */}
|
|
|
|
{!completeData.summary && !completeData.result && !completeData.attachments && !completeData.tasksCompleted && !isStreaming && (
|
|
|
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
|
|
|
<div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
|
|
|
<CheckCircle2 className="h-8 w-8 text-muted-foreground" />
|
|
|
|
</div>
|
|
|
|
<h3 className="text-lg font-medium text-foreground mb-2">
|
|
|
|
Task Completed
|
|
|
|
</h3>
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
No additional details provided
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</ScrollArea>
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
<div className="px-4 py-2 h-10 bg-gradient-to-r from-zinc-50/90 to-zinc-100/90 dark:from-zinc-900/90 dark:to-zinc-800/90 backdrop-blur-sm border-t border-zinc-200 dark:border-zinc-800 flex justify-between items-center gap-4">
|
|
|
|
<div className="h-full flex items-center gap-2 text-sm text-zinc-500 dark:text-zinc-400">
|
|
|
|
<Badge className="h-6 py-0.5" variant="outline">
|
|
|
|
<CheckCircle2 className="h-3 w-3 mr-1" />
|
|
|
|
Task Completion
|
|
|
|
</Badge>
|
|
|
|
</div>
|
2025-05-28 04:12:19 +08:00
|
|
|
|
2025-05-25 23:33:50 +08:00
|
|
|
<div className="text-xs text-zinc-500 dark:text-zinc-400">
|
|
|
|
{toolTimestamp && !isStreaming
|
|
|
|
? formatTimestamp(toolTimestamp)
|
|
|
|
: assistantTimestamp
|
|
|
|
? formatTimestamp(assistantTimestamp)
|
|
|
|
: ''}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Card>
|
|
|
|
);
|
|
|
|
}
|