fix: visual improvements stream

This commit is contained in:
Vukasin 2025-07-22 20:10:17 +02:00
parent d4b031d00e
commit f33f5f6c6e
4 changed files with 89 additions and 59 deletions

View File

@ -0,0 +1,50 @@
import React from 'react';
import { motion } from 'framer-motion';
import { CircleDashed } from 'lucide-react';
import { extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
import { getToolIcon, getUserFriendlyToolName } from '@/components/thread/utils';
interface ShowToolStreamProps {
content: string;
}
export const ShowToolStream: React.FC<ShowToolStreamProps> = ({ content }) => {
const toolName = extractToolNameFromStream(content);
if (!toolName) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="mt-2 mb-1"
>
<div className="animate-shimmer inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs font-medium text-primary bg-primary/10 rounded-lg border border-primary/20">
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
</div>
<span className="font-mono text-xs text-primary">Processing...</span>
</div>
</motion.div>
);
}
const IconComponent = getToolIcon(toolName);
const displayName = getUserFriendlyToolName(toolName);
return (
<motion.div
layoutId={`tool-stream-${toolName}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
className="mt-2 mb-1"
>
<div className="animate-shimmer inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs font-medium text-primary bg-primary/10 rounded-lg border border-primary/20">
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
</div>
<span className="font-mono text-xs text-primary">{displayName}</span>
<span className="ml-1 text-primary/70 text-xs">running...</span>
</div>
</motion.div>
);
};

View File

@ -18,6 +18,8 @@ import { KortixLogo } from '@/components/sidebar/kortix-logo';
import { AgentLoader } from './loader';
import { parseXmlToolCalls, isNewXmlFormat, extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
import { parseToolResult } from '@/components/thread/tool-views/tool-result-parser';
import { ShowToolStream } from './ShowToolStream';
import { motion } from 'framer-motion';
// Define the set of tags whose raw XML should be hidden during streaming
const HIDE_STREAMING_XML_TAGS = new Set([
@ -166,7 +168,14 @@ export function renderMarkdownContent(
}
contentParts.push(
<div key={`tool-${match.index}-${index}`} className="my-1">
<motion.div
key={`tool-${match.index}-${index}`}
className="my-1"
layoutId={`tool-stream-${toolName}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<button
onClick={() => handleToolClick(messageId, toolName)}
className="inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs text-muted-foreground bg-muted hover:bg-muted/80 rounded-lg transition-colors cursor-pointer border border-neutral-200 dark:border-neutral-700/50"
@ -177,7 +186,7 @@ export function renderMarkdownContent(
<span className="font-mono text-xs text-foreground">{getUserFriendlyToolName(toolName)}</span>
{paramDisplay && <span className="ml-1 text-muted-foreground truncate max-w-[200px]" title={paramDisplay}>{paramDisplay}</span>}
</button>
</div>
</motion.div>
);
}
});
@ -248,7 +257,14 @@ export function renderMarkdownContent(
// Render tool button as a clickable element
contentParts.push(
<div key={toolCallKey} className="my-1">
<motion.div
key={toolCallKey}
className="my-1"
layoutId={`tool-stream-${toolName}`}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<button
onClick={() => handleToolClick(messageId, toolName)}
className="inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs text-muted-foreground bg-muted hover:bg-muted/80 rounded-lg transition-colors cursor-pointer border border-neutral-200 dark:border-neutral-700/50"
@ -259,7 +275,7 @@ export function renderMarkdownContent(
<span className="font-mono text-xs text-foreground">{getUserFriendlyToolName(toolName)}</span>
{paramDisplay && <span className="ml-1 text-muted-foreground truncate max-w-[200px]" title={paramDisplay}>{paramDisplay}</span>}
</button>
</div>
</motion.div>
);
}
lastIndex = xmlRegex.lastIndex;
@ -764,35 +780,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
)}
{detectedTag && detectedTag !== 'function_calls' && (
<div className="mt-2 mb-1">
<button
className="animate-shimmer inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs font-medium text-primary bg-muted hover:bg-muted/80 rounded-md transition-colors cursor-pointer border border-primary/20"
>
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
</div>
<span className="font-mono text-xs text-primary">{getUserFriendlyToolName(detectedTag)}</span>
</button>
</div>
)}
{detectedTag === 'function_calls' && (
<div className="mt-2 mb-1">
<button
className="animate-shimmer inline-flex items-center gap-1.5 py-1 px-1 pr-1.5 text-xs font-medium text-primary bg-muted hover:bg-muted/80 rounded-md transition-colors cursor-pointer border border-primary/20"
>
<div className='border-2 bg-gradient-to-br from-neutral-200 to-neutral-300 dark:from-neutral-700 dark:to-neutral-800 flex items-center justify-center p-0.5 rounded-sm border-neutral-400/20 dark:border-neutral-600'>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
</div>
<span className="font-mono text-xs text-primary">
{(() => {
const extractedToolName = extractToolNameFromStream(streamingTextContent);
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
})()}
</span>
</button>
</div>
{detectedTag && (
<ShowToolStream content={textToRender.substring(tagStartIndex)} />
)}
{streamingToolCall && !detectedTag && (
@ -867,22 +856,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
)}
{detectedTag && (
<div className="mt-2 mb-1">
<button
className="animate-shimmer inline-flex items-center gap-1.5 py-1 px-2.5 text-xs font-medium text-primary bg-primary/10 hover:bg-primary/20 rounded-md transition-colors cursor-pointer border border-primary/20"
>
<CircleDashed className="h-3.5 w-3.5 text-primary flex-shrink-0 animate-spin animation-duration-2000" />
<span className="font-mono text-xs text-primary">
{detectedTag === 'function_calls' ?
(() => {
const extractedToolName = extractToolNameFromStream(streamingText);
return extractedToolName ? getUserFriendlyToolName(extractedToolName) : 'Using Tool...';
})() :
getUserFriendlyToolName(detectedTag)
}
</span>
</button>
</div>
<ShowToolStream content={textToRender.substring(tagStartIndex)} />
)}
</>
)}

View File

@ -163,15 +163,15 @@ export function AskToolView({
sandboxId={project?.sandbox?.id}
showPreview={true}
className={cn(
"w-full",
isImage ? "h-auto min-h-[54px]" :
isPreviewable ? "min-h-[240px] max-h-[400px] overflow-auto" : "h-[54px]"
isImage ? "aspect-square w-full" : "w-full",
isImage ? "" :
isPreviewable ? "min-h-full max-h-[400px] overflow-auto" : "h-[54px]"
)}
customStyle={
isImage ? {
width: '100%',
height: 'auto',
'--attachment-height': shouldSpanFull ? '240px' : '180px'
height: '100%',
'--attachment-height': '100%'
} as React.CSSProperties :
isPreviewable ? {
gridColumn: '1 / -1'

View File

@ -292,13 +292,19 @@ export function FileOperationToolView({
</a>
</Button>
)}
<TabsList className="-mr-2 h-7 bg-zinc-100/70 dark:bg-zinc-800/70 rounded-lg">
<TabsTrigger value="code" className="rounded-md data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-900 data-[state=active]:text-primary">
<Code className="h-4 w-4" />
<TabsList className="h-8 bg-muted/50 border border-border/50 p-0.5 gap-1">
<TabsTrigger
value="code"
className="flex items-center gap-1.5 px-4 py-2 text-xs font-medium transition-all [&[data-state=active]]:bg-primary/10 [&[data-state=active]]:text-foreground hover:bg-background/50 text-muted-foreground"
>
<Code className="h-3.5 w-3.5" />
Source
</TabsTrigger>
<TabsTrigger value="preview" className="rounded-md data-[state=active]:bg-white dark:data-[state=active]:bg-zinc-900 data-[state=active]:text-primary">
<Eye className="h-4 w-4" />
<TabsTrigger
value="preview"
className="flex items-center gap-1.5 px-4 py-2 text-xs font-medium transition-all [&[data-state=active]]:bg-primary/10 [&[data-state=active]]:text-foreground hover:bg-background/50 text-muted-foreground"
>
<Eye className="h-3.5 w-3.5" />
Preview
</TabsTrigger>
</TabsList>