mirror of https://github.com/kortix-ai/suna.git
feat: tool-panel animation
This commit is contained in:
parent
88cca815f6
commit
f6cd8779ed
|
@ -664,6 +664,13 @@ export default function ThreadPage({
|
|||
agentName={agent && agent.name}
|
||||
selectedAgentId={selectedAgentId}
|
||||
onAgentSelect={setSelectedAgentId}
|
||||
toolCalls={toolCalls}
|
||||
toolCallIndex={currentToolIndex}
|
||||
showToolPreview={!isSidePanelOpen && toolCalls.length > 0}
|
||||
onExpandToolPreview={() => {
|
||||
setIsSidePanelOpen(true);
|
||||
userClosedPanelRef.current = false;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -117,7 +117,6 @@ export function ThreadLayout({
|
|||
isLoading={!initialLoadCompleted || isLoading}
|
||||
onFileClick={onViewFiles}
|
||||
agentName={agentName}
|
||||
onToggleFloatingPreview={() => onToggleSidePanel()}
|
||||
/>
|
||||
|
||||
{sandboxId && (
|
||||
|
|
|
@ -513,14 +513,6 @@ export default function ThreadPage({
|
|||
};
|
||||
}, [threadId]);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!messagesContainerRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } =
|
||||
messagesContainerRef.current;
|
||||
const isScrolledUp = scrollHeight - scrollTop - clientHeight > 100;
|
||||
setShowScrollButton(isScrolledUp);
|
||||
setUserHasScrolled(isScrolledUp);
|
||||
};
|
||||
|
||||
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior });
|
||||
|
@ -806,7 +798,6 @@ export default function ThreadPage({
|
|||
externalNavigateToIndex={externalNavIndex}
|
||||
project={project}
|
||||
onFileClick={handleOpenFileViewer}
|
||||
onToggleFloatingPreview={() => setIsSidePanelOpen(true)}
|
||||
/>
|
||||
|
||||
<FileViewerModal
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useModelSelection } from './_use-model-selection';
|
|||
import { AgentSelector } from './agent-selector';
|
||||
import { useFileDelete } from '@/hooks/react-query/files';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
|
||||
|
||||
export interface ChatInputHandles {
|
||||
getPendingFiles: () => File[];
|
||||
|
@ -44,6 +45,10 @@ export interface ChatInputProps {
|
|||
agentName?: string;
|
||||
messages?: any[];
|
||||
bgColor?: string;
|
||||
toolCalls?: ToolCallInput[];
|
||||
toolCallIndex?: number;
|
||||
showToolPreview?: boolean;
|
||||
onExpandToolPreview?: () => void;
|
||||
}
|
||||
|
||||
export interface UploadedFile {
|
||||
|
@ -74,6 +79,10 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
agentName,
|
||||
messages = [],
|
||||
bgColor = 'bg-sidebar',
|
||||
toolCalls = [],
|
||||
toolCallIndex = 0,
|
||||
showToolPreview = false,
|
||||
onExpandToolPreview,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
@ -226,15 +235,21 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-4xl">
|
||||
<FloatingToolPreview
|
||||
toolCalls={toolCalls}
|
||||
currentIndex={toolCallIndex}
|
||||
onExpand={onExpandToolPreview || (() => {})}
|
||||
agentName={agentName}
|
||||
isVisible={showToolPreview}
|
||||
/>
|
||||
<Card
|
||||
className="shadow-none w-full max-w-4xl mx-auto bg-transparent border-none rounded-xl overflow-hidden"
|
||||
className="-mb-2 bg-red-400 shadow-none w-full max-w-4xl mx-auto bg-transparent border-none rounded-xl overflow-hidden"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDraggingOver(false);
|
||||
|
||||
if (fileInputRef.current && e.dataTransfer.files.length > 0) {
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFiles(
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { CircleDashed, Maximize2 } from 'lucide-react';
|
||||
import { getToolIcon, getUserFriendlyToolName } from '@/components/thread/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export interface ToolCallInput {
|
||||
assistantCall: {
|
||||
content?: string;
|
||||
name?: string;
|
||||
timestamp?: string;
|
||||
};
|
||||
toolResult?: {
|
||||
content?: string;
|
||||
isSuccess?: boolean;
|
||||
timestamp?: string;
|
||||
};
|
||||
messages?: any[];
|
||||
}
|
||||
|
||||
interface FloatingToolPreviewProps {
|
||||
toolCalls: ToolCallInput[];
|
||||
currentIndex: number;
|
||||
onExpand: () => void;
|
||||
agentName?: string;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
const FLOATING_LAYOUT_ID = 'tool-panel-float';
|
||||
const CONTENT_LAYOUT_ID = 'tool-panel-content';
|
||||
|
||||
const getToolResultStatus = (toolCall: any): boolean => {
|
||||
const content = toolCall?.toolResult?.content;
|
||||
if (!content) return toolCall?.toolResult?.isSuccess ?? true;
|
||||
|
||||
const safeParse = (data: any) => {
|
||||
try { return typeof data === 'string' ? JSON.parse(data) : data; }
|
||||
catch { return null; }
|
||||
};
|
||||
|
||||
const parsed = safeParse(content);
|
||||
if (!parsed) return toolCall?.toolResult?.isSuccess ?? true;
|
||||
|
||||
if (parsed.content) {
|
||||
const inner = safeParse(parsed.content);
|
||||
if (inner?.tool_execution?.result?.success !== undefined) {
|
||||
return inner.tool_execution.result.success;
|
||||
}
|
||||
}
|
||||
const success = parsed.tool_execution?.result?.success ??
|
||||
parsed.result?.success ??
|
||||
parsed.success;
|
||||
|
||||
return success !== undefined ? success : (toolCall?.toolResult?.isSuccess ?? true);
|
||||
};
|
||||
|
||||
export const FloatingToolPreview: React.FC<FloatingToolPreviewProps> = ({
|
||||
toolCalls,
|
||||
currentIndex,
|
||||
onExpand,
|
||||
agentName,
|
||||
isVisible,
|
||||
}) => {
|
||||
const [isExpanding, setIsExpanding] = React.useState(false);
|
||||
const currentToolCall = toolCalls[currentIndex];
|
||||
const totalCalls = toolCalls.length;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isVisible) {
|
||||
setIsExpanding(false);
|
||||
}
|
||||
}, [isVisible]);
|
||||
|
||||
if (!currentToolCall || totalCalls === 0) return null;
|
||||
|
||||
const toolName = currentToolCall.assistantCall?.name || 'Tool Call';
|
||||
const CurrentToolIcon = getToolIcon(toolName);
|
||||
const isStreaming = currentToolCall.toolResult?.content === 'STREAMING';
|
||||
const isSuccess = isStreaming ? true : getToolResultStatus(currentToolCall);
|
||||
|
||||
const handleClick = () => {
|
||||
setIsExpanding(true);
|
||||
requestAnimationFrame(() => {
|
||||
onExpand();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
layoutId={FLOATING_LAYOUT_ID}
|
||||
layout
|
||||
transition={{
|
||||
layout: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}
|
||||
}}
|
||||
className="-mb-4 w-full"
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<motion.div
|
||||
layoutId={CONTENT_LAYOUT_ID}
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="bg-sidebar border border-border rounded-2xl p-2 w-full cursor-pointer group"
|
||||
onClick={handleClick}
|
||||
style={{ opacity: isExpanding ? 0 : 1 }}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
layoutId="tool-icon"
|
||||
className={cn(
|
||||
"w-10 h-10 rounded-xl flex items-center justify-center",
|
||||
isStreaming
|
||||
? "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
|
||||
: isSuccess
|
||||
? "bg-green-50 dark:bg-green-900/20 border border-green-300 dark:border-green-800"
|
||||
: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
)}
|
||||
style={{ opacity: isExpanding ? 0 : 1 }}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<CircleDashed className="h-5 w-5 text-blue-500 dark:text-blue-400 animate-spin" style={{ opacity: isExpanding ? 0 : 1 }} />
|
||||
) : (
|
||||
<CurrentToolIcon className="h-5 w-5 text-foreground" style={{ opacity: isExpanding ? 0 : 1 }} />
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0" style={{ opacity: isExpanding ? 0 : 1 }}>
|
||||
<motion.div layoutId="tool-title" className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{getUserFriendlyToolName(toolName)}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{currentIndex + 1}/{totalCalls}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div layoutId="tool-status" className="flex items-center gap-2">
|
||||
<div className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
isStreaming
|
||||
? "bg-blue-500 animate-pulse"
|
||||
: isSuccess
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
)} />
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{isStreaming
|
||||
? `${agentName || 'Suna'} is working...`
|
||||
: isSuccess
|
||||
? "Success"
|
||||
: "Failed"
|
||||
}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<Button value='ghost' className="bg-transparent hover:bg-transparent flex-shrink-0" style={{ opacity: isExpanding ? 0 : 1 }}>
|
||||
<Maximize2 className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
|
@ -48,16 +48,6 @@ interface ToolCallSidePanelProps {
|
|||
isLoading?: boolean;
|
||||
agentName?: string;
|
||||
onFileClick?: (filePath: string) => void;
|
||||
showFloatingPreview?: boolean;
|
||||
onToggleFloatingPreview?: () => void;
|
||||
}
|
||||
|
||||
interface FloatingPreviewProps {
|
||||
toolCalls: ToolCallInput[];
|
||||
currentIndex: number;
|
||||
onExpand: () => void;
|
||||
agentName?: string;
|
||||
isVisible: boolean;
|
||||
}
|
||||
|
||||
interface ToolCallSnapshot {
|
||||
|
@ -70,110 +60,6 @@ interface ToolCallSnapshot {
|
|||
const FLOATING_LAYOUT_ID = 'tool-panel-float';
|
||||
const CONTENT_LAYOUT_ID = 'tool-panel-content';
|
||||
|
||||
const FloatingPreview: React.FC<FloatingPreviewProps> = ({
|
||||
toolCalls,
|
||||
currentIndex,
|
||||
onExpand,
|
||||
agentName,
|
||||
isVisible,
|
||||
}) => {
|
||||
const currentToolCall = toolCalls[currentIndex];
|
||||
const totalCalls = toolCalls.length;
|
||||
|
||||
if (!currentToolCall || totalCalls === 0) return null;
|
||||
|
||||
const toolName = currentToolCall.assistantCall?.name || 'Tool Call';
|
||||
const CurrentToolIcon = getToolIcon(toolName);
|
||||
const isStreaming = currentToolCall.toolResult?.content === 'STREAMING';
|
||||
const isSuccess = currentToolCall.toolResult?.isSuccess ?? true;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible && (
|
||||
<motion.div
|
||||
layoutId={FLOATING_LAYOUT_ID}
|
||||
layout
|
||||
transition={{
|
||||
layout: {
|
||||
type: "spring",
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
}
|
||||
}}
|
||||
className="fixed bottom-24 left-1/2 transform -translate-x-1/2 z-40"
|
||||
style={{ pointerEvents: 'auto' }}
|
||||
>
|
||||
<motion.div
|
||||
layoutId={CONTENT_LAYOUT_ID}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="bg-background/95 backdrop-blur-md border border-border/50 shadow-2xl rounded-2xl p-4 max-w-sm mx-auto cursor-pointer group"
|
||||
onClick={onExpand}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<motion.div
|
||||
layoutId="tool-icon"
|
||||
className={cn(
|
||||
"w-10 h-10 rounded-xl flex items-center justify-center",
|
||||
isStreaming
|
||||
? "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
|
||||
: isSuccess
|
||||
? "bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800"
|
||||
: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||
)}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<CircleDashed className="h-5 w-5 text-blue-500 dark:text-blue-400 animate-spin" />
|
||||
) : (
|
||||
<CurrentToolIcon className="h-5 w-5 text-foreground" />
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<motion.div layoutId="tool-title" className="flex items-center gap-2 mb-1">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{getUserFriendlyToolName(toolName)}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{currentIndex + 1}/{totalCalls}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div layoutId="tool-status" className="flex items-center gap-2">
|
||||
<div className={cn(
|
||||
"w-2 h-2 rounded-full",
|
||||
isStreaming
|
||||
? "bg-blue-500 animate-pulse"
|
||||
: isSuccess
|
||||
? "bg-green-500"
|
||||
: "bg-red-500"
|
||||
)} />
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{isStreaming
|
||||
? `${agentName || 'Suna'} is working...`
|
||||
: isSuccess
|
||||
? "Completed successfully"
|
||||
: "Failed to execute"
|
||||
}
|
||||
</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Maximize2 className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
export function ToolCallSidePanel({
|
||||
isOpen,
|
||||
onClose,
|
||||
|
@ -187,25 +73,17 @@ export function ToolCallSidePanel({
|
|||
externalNavigateToIndex,
|
||||
agentName,
|
||||
onFileClick,
|
||||
showFloatingPreview = true,
|
||||
onToggleFloatingPreview,
|
||||
}: ToolCallSidePanelProps) {
|
||||
const [dots, setDots] = React.useState('');
|
||||
const [internalIndex, setInternalIndex] = React.useState(0);
|
||||
const [navigationMode, setNavigationMode] = React.useState<'live' | 'manual'>('live');
|
||||
const [toolCallSnapshots, setToolCallSnapshots] = React.useState<ToolCallSnapshot[]>([]);
|
||||
const [isInitialized, setIsInitialized] = React.useState(false);
|
||||
const [showFloatingPreviewState, setShowFloatingPreviewState] = React.useState(false);
|
||||
const [isExiting, setIsExiting] = React.useState(false);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setIsExiting(true);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
setIsExiting(false);
|
||||
}, 400);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -523,20 +401,6 @@ export function ToolCallSidePanel({
|
|||
}
|
||||
}, [externalNavigateToIndex, totalCalls, internalNavigate]);
|
||||
|
||||
// Show floating preview when side panel is closed and there are tool calls
|
||||
React.useEffect(() => {
|
||||
const hasToolCalls = toolCallSnapshots.length > 0;
|
||||
const shouldShowFloating = !isOpen && hasToolCalls && showFloatingPreview && !isExiting;
|
||||
setShowFloatingPreviewState(shouldShowFloating);
|
||||
}, [isOpen, toolCallSnapshots.length, showFloatingPreview, isExiting]);
|
||||
|
||||
const handleExpandFromFloating = React.useCallback(() => {
|
||||
// This will be handled by the parent component through onClose callback
|
||||
if (onToggleFloatingPreview) {
|
||||
onToggleFloatingPreview();
|
||||
}
|
||||
}, [onToggleFloatingPreview]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isStreaming) return;
|
||||
const interval = setInterval(() => {
|
||||
|
@ -549,57 +413,51 @@ export function ToolCallSidePanel({
|
|||
return () => clearInterval(interval);
|
||||
}, [isStreaming]);
|
||||
|
||||
// Render floating preview when side panel is closed
|
||||
if (!isOpen && !isExiting) {
|
||||
return (
|
||||
<FloatingPreview
|
||||
toolCalls={toolCalls}
|
||||
currentIndex={safeInternalIndex}
|
||||
onExpand={handleExpandFromFloating}
|
||||
agentName={agentName}
|
||||
isVisible={showFloatingPreviewState}
|
||||
/>
|
||||
);
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-y-0 right-0 border-l flex flex-col z-30 h-screen transition-all duration-200 ease-in-out',
|
||||
isMobile
|
||||
? 'w-full'
|
||||
: 'w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px]',
|
||||
!isOpen && 'translate-x-full',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-background">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="pt-4 pl-4 pr-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="ml-2 flex items-center gap-2">
|
||||
<Computer className="h-4 w-4" />
|
||||
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
|
||||
</h2>
|
||||
<div className="fixed inset-0 z-30 pointer-events-none">
|
||||
<div className="p-4 h-full flex items-stretch justify-end pointer-events-auto">
|
||||
<div
|
||||
className={cn(
|
||||
'border rounded-2xl flex flex-col shadow-2xl bg-background',
|
||||
isMobile
|
||||
? 'w-full'
|
||||
: 'w-[90%] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px]',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="pt-4 pl-4 pr-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="ml-2 flex items-center gap-2">
|
||||
<Computer className="h-4 w-4" />
|
||||
<h2 className="text-md font-medium text-zinc-900 dark:text-zinc-100">
|
||||
{agentName ? `${agentName}'s Computer` : 'Suna\'s Computer'}
|
||||
</h2>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClose}
|
||||
className="h-8 w-8"
|
||||
title="Minimize to floating preview"
|
||||
>
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-20 w-full rounded-md" />
|
||||
<Skeleton className="h-40 w-full rounded-md" />
|
||||
<Skeleton className="h-20 w-full rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleClose}
|
||||
className="h-8 w-8"
|
||||
title="Minimize to floating preview"
|
||||
>
|
||||
<Minimize2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-auto">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-20 w-full rounded-md" />
|
||||
<Skeleton className="h-40 w-full rounded-md" />
|
||||
<Skeleton className="h-20 w-full rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -829,20 +687,24 @@ export function ToolCallSidePanel({
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{
|
||||
opacity: { duration: 0.2 }
|
||||
opacity: { duration: 0.15 },
|
||||
layout: {
|
||||
type: "spring",
|
||||
stiffness: 400,
|
||||
damping: 35
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'fixed inset-y-0 right-0 border-l flex flex-col z-30 h-screen',
|
||||
'fixed top-2 right-2 bottom-4 border rounded-2xl flex flex-col z-30',
|
||||
isMobile
|
||||
? 'w-full'
|
||||
: 'w-[40vw] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[650px]',
|
||||
? 'left-2'
|
||||
: 'w-[40vw] sm:w-[450px] md:w-[500px] lg:w-[550px] xl:w-[645px]',
|
||||
)}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.18)'
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-background">
|
||||
<div className="flex-1 flex flex-col overflow-hidden bg-sidebar">
|
||||
{renderContent()}
|
||||
</div>
|
||||
{(displayTotalCalls > 1 || (isCurrentToolStreaming && totalCompletedCalls > 0)) && (
|
||||
|
|
|
@ -507,18 +507,26 @@ export function extractFileContent(
|
|||
return null;
|
||||
}
|
||||
|
||||
// Helper to process and clean file content
|
||||
function processFileContent(content: string): string {
|
||||
if (!content) return content;
|
||||
|
||||
// Handle escaped characters
|
||||
const trimmedContent = content.trim();
|
||||
const isLikelyJson = (trimmedContent.startsWith('{') && trimmedContent.endsWith('}')) ||
|
||||
(trimmedContent.startsWith('[') && trimmedContent.endsWith(']'));
|
||||
|
||||
if (isLikelyJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return content
|
||||
.replace(/\\n/g, '\n') // Replace \n with actual newlines
|
||||
.replace(/\\t/g, '\t') // Replace \t with actual tabs
|
||||
.replace(/\\r/g, '') // Remove \r
|
||||
.replace(/\\\\/g, '\\') // Replace \\ with \
|
||||
.replace(/\\"/g, '"') // Replace \" with "
|
||||
.replace(/\\'/g, "'"); // Replace \' with '
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\r/g, '')
|
||||
.replace(/\\\\/g, '\\')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\'/g, "'");
|
||||
}
|
||||
|
||||
// Helper to determine file type (for syntax highlighting)
|
||||
|
|
Loading…
Reference in New Issue