feat: integrate tool call icons with status indicators and daisy chaining (#4279)
Co-authored-by: dianed-square <73617011+dianed-square@users.noreply.github.com> Co-authored-by: Zane <75694352+zanesq@users.noreply.github.com> Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
@@ -7,6 +7,12 @@ import { extractImagePaths, removeImagePathsFromText } from '../utils/imageUtils
|
||||
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||
import ToolCallChain from './ToolCallChain';
|
||||
import {
|
||||
identifyConsecutiveToolCalls,
|
||||
shouldHideMessage,
|
||||
getChainForMessage,
|
||||
} from '../utils/toolCallChaining';
|
||||
import {
|
||||
Message,
|
||||
getTextContent,
|
||||
@@ -18,6 +24,7 @@ import {
|
||||
import ToolCallConfirmation from './ToolCallConfirmation';
|
||||
import MessageCopyLink from './MessageCopyLink';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface GooseMessageProps {
|
||||
// messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests
|
||||
@@ -60,18 +67,23 @@ export default function GooseMessage({
|
||||
}
|
||||
|
||||
const cotRaw = match[1].trim();
|
||||
const visible = text.replace(match[0], '').trim();
|
||||
return { visibleText: visible, cotText: cotRaw.length > 0 ? cotRaw : null };
|
||||
const visibleText = text.replace(regex, '').trim();
|
||||
|
||||
return {
|
||||
visibleText,
|
||||
cotText: cotRaw || null,
|
||||
};
|
||||
};
|
||||
|
||||
const { visibleText: textWithoutCot, cotText } = splitChainOfThought(textContent);
|
||||
// Split out Chain-of-Thought
|
||||
const { visibleText, cotText } = splitChainOfThought(textContent);
|
||||
|
||||
// Extract image paths from the visible part of the message (exclude CoT)
|
||||
const imagePaths = extractImagePaths(textWithoutCot);
|
||||
// Extract image paths from the message content
|
||||
const imagePaths = extractImagePaths(visibleText);
|
||||
|
||||
// Remove image paths from text for display
|
||||
const displayText =
|
||||
imagePaths.length > 0 ? removeImagePathsFromText(textWithoutCot, imagePaths) : textWithoutCot;
|
||||
imagePaths.length > 0 ? removeImagePathsFromText(visibleText, imagePaths) : visibleText;
|
||||
|
||||
// Memoize the timestamp
|
||||
const timestamp = useMemo(() => formatMessageTimestamp(message.created), [message.created]);
|
||||
@@ -79,11 +91,53 @@ export default function GooseMessage({
|
||||
// Get tool requests from the message
|
||||
const toolRequests = getToolRequests(message);
|
||||
|
||||
// Get current message index
|
||||
const messageIndex = messages.findIndex((msg) => msg.id === message.id);
|
||||
|
||||
// Enhanced chain detection that works during streaming
|
||||
const toolCallChains = useMemo(() => {
|
||||
// Always run chain detection, but handle streaming messages specially
|
||||
const chains = identifyConsecutiveToolCalls(messages);
|
||||
|
||||
// If this message is streaming and has tool calls but no text,
|
||||
// check if it should extend an existing chain
|
||||
if (isStreaming && toolRequests.length > 0 && !displayText.trim()) {
|
||||
// Look for an existing chain that this message could extend
|
||||
const previousMessage = messageIndex > 0 ? messages[messageIndex - 1] : null;
|
||||
if (previousMessage) {
|
||||
const prevToolRequests = getToolRequests(previousMessage);
|
||||
|
||||
// If previous message has tool calls (with or without text), extend its chain
|
||||
if (prevToolRequests.length > 0) {
|
||||
// Find if previous message is part of a chain
|
||||
const prevChain = chains.find((chain) => chain.includes(messageIndex - 1));
|
||||
if (prevChain) {
|
||||
// Extend the existing chain to include this streaming message
|
||||
const extendedChains = chains.map((chain) =>
|
||||
chain === prevChain ? [...chain, messageIndex] : chain
|
||||
);
|
||||
return extendedChains;
|
||||
} else {
|
||||
// Create a new chain with previous and current message
|
||||
return [...chains, [messageIndex - 1, messageIndex]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chains;
|
||||
}, [messages, isStreaming, messageIndex, toolRequests, displayText]);
|
||||
|
||||
// Check if this message should be hidden (part of chain but not first)
|
||||
const shouldHide = shouldHideMessage(messageIndex, toolCallChains);
|
||||
|
||||
// Get the chain this message belongs to
|
||||
const messageChain = getChainForMessage(messageIndex, toolCallChains);
|
||||
|
||||
// Extract URLs under a few conditions
|
||||
// 1. The message is purely text
|
||||
// 2. The link wasn't also present in the previous message
|
||||
// 3. The message contains the explicit http:// or https:// protocol at the beginning
|
||||
const messageIndex = messages?.findIndex((msg) => msg.id === message.id);
|
||||
const previousMessage = messageIndex > 0 ? messages[messageIndex - 1] : null;
|
||||
const previousUrls = previousMessage ? extractUrls(getTextContent(previousMessage)) : [];
|
||||
const urls = toolRequests.length === 0 ? extractUrls(displayText, previousUrls) : [];
|
||||
@@ -145,10 +199,17 @@ export default function GooseMessage({
|
||||
appendMessage,
|
||||
]);
|
||||
|
||||
// If this message should be hidden (part of chain but not first), don't render it
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine rendering logic based on chain membership and content
|
||||
const isFirstInChain = messageChain && messageChain[0] === messageIndex;
|
||||
|
||||
return (
|
||||
<div className="goose-message flex w-[90%] justify-start min-w-0">
|
||||
<div className="flex flex-col w-full min-w-0">
|
||||
{/* Chain-of-Thought (hidden by default) */}
|
||||
{cotText && (
|
||||
<details className="bg-bgSubtle border border-borderSubtle rounded p-2 mb-2">
|
||||
<summary className="cursor-pointer text-sm text-textSubtle select-none">
|
||||
@@ -160,61 +221,73 @@ export default function GooseMessage({
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Visible assistant response */}
|
||||
{displayText && (
|
||||
<div className="flex flex-col group">
|
||||
<div className={`goose-message-content py-2`}>
|
||||
<div ref={contentRef}>{<MarkdownContent content={displayText} />}</div>
|
||||
<div ref={contentRef} className="w-full">
|
||||
<MarkdownContent content={displayText} />
|
||||
</div>
|
||||
|
||||
{/* Render images if any */}
|
||||
{/* Image previews */}
|
||||
{imagePaths.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2 mb-2">
|
||||
<div className="mt-4">
|
||||
{imagePaths.map((imagePath, index) => (
|
||||
<ImagePreview key={index} src={imagePath} alt={`Image ${index + 1}`} />
|
||||
<ImagePreview key={index} src={imagePath} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Only show timestamp and copy link when not streaming */}
|
||||
<div className="relative flex justify-start">
|
||||
{toolRequests.length === 0 && !isStreaming && (
|
||||
<div className="text-xs font-mono text-text-muted pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{timestamp}
|
||||
</div>
|
||||
)}
|
||||
{displayText &&
|
||||
message.content.every((content) => content.type === 'text') &&
|
||||
!isStreaming && (
|
||||
{toolRequests.length === 0 && (
|
||||
<div className="relative flex justify-start">
|
||||
{!isStreaming && (
|
||||
<div className="text-xs font-mono text-text-muted pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{timestamp}
|
||||
</div>
|
||||
)}
|
||||
{message.content.every((content) => content.type === 'text') && !isStreaming && (
|
||||
<div className="absolute left-0 pt-1">
|
||||
<MessageCopyLink text={displayText} contentRef={contentRef} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{toolRequests.length > 0 && (
|
||||
<div className="relative flex flex-col w-full">
|
||||
{toolRequests.map((toolRequest) => (
|
||||
<div className={`goose-message-tool pb-2`} key={toolRequest.id}>
|
||||
<ToolCallWithResponse
|
||||
// If the message is resumed and not matched tool response, it means the tool is broken or cancelled.
|
||||
isCancelledMessage={
|
||||
messageIndex < messageHistoryIndex &&
|
||||
toolResponsesMap.get(toolRequest.id) == undefined
|
||||
}
|
||||
toolRequest={toolRequest}
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
notifications={toolCallNotifications.get(toolRequest.id)}
|
||||
isStreamingMessage={isStreaming}
|
||||
append={append}
|
||||
/>
|
||||
<div className={cn(displayText && 'mt-2')}>
|
||||
{isFirstInChain ? (
|
||||
<ToolCallChain
|
||||
messages={messages}
|
||||
chainIndices={messageChain}
|
||||
toolCallNotifications={toolCallNotifications}
|
||||
toolResponsesMap={toolResponsesMap}
|
||||
messageHistoryIndex={messageHistoryIndex}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
) : !messageChain ? (
|
||||
<div className="relative flex flex-col w-full">
|
||||
<div className="flex flex-col gap-3">
|
||||
{toolRequests.map((toolRequest) => (
|
||||
<div className="goose-message-tool" key={toolRequest.id}>
|
||||
<ToolCallWithResponse
|
||||
isCancelledMessage={
|
||||
messageIndex < messageHistoryIndex &&
|
||||
toolResponsesMap.get(toolRequest.id) == undefined
|
||||
}
|
||||
toolRequest={toolRequest}
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
notifications={toolCallNotifications.get(toolRequest.id)}
|
||||
isStreamingMessage={isStreaming}
|
||||
append={append}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{!isStreaming && timestamp}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-xs text-text-muted pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{!isStreaming && timestamp}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -229,9 +302,8 @@ export default function GooseMessage({
|
||||
</div>
|
||||
|
||||
{/* TODO(alexhancock): Re-enable link previews once styled well again */}
|
||||
{/* eslint-disable-next-line no-constant-binary-expression */}
|
||||
{false && urls.length > 0 && (
|
||||
<div className="flex flex-wrap mt-[16px]">
|
||||
{urls.length > 0 && (
|
||||
<div className="mt-4">
|
||||
{urls.map((url, index) => (
|
||||
<LinkPreview key={index} url={url} />
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { formatMessageTimestamp } from '../utils/timeUtils';
|
||||
import { Message, getToolRequests } from '../types/message';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import ToolCallWithResponse from './ToolCallWithResponse';
|
||||
|
||||
interface ToolCallChainProps {
|
||||
messages: Message[];
|
||||
chainIndices: number[];
|
||||
toolCallNotifications: Map<string, NotificationEvent[]>;
|
||||
toolResponsesMap: Map<string, import('../types/message').ToolResponseMessageContent>;
|
||||
messageHistoryIndex: number;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export default function ToolCallChain({
|
||||
messages,
|
||||
chainIndices,
|
||||
toolCallNotifications,
|
||||
toolResponsesMap,
|
||||
messageHistoryIndex,
|
||||
isStreaming = false,
|
||||
}: ToolCallChainProps) {
|
||||
const lastMessageIndex = chainIndices[chainIndices.length - 1];
|
||||
const lastMessage = messages[lastMessageIndex];
|
||||
const timestamp = lastMessage ? formatMessageTimestamp(lastMessage.created) : '';
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full">
|
||||
<div className="flex flex-col gap-3">
|
||||
{chainIndices.map((messageIndex) => {
|
||||
const message = messages[messageIndex];
|
||||
const toolRequests = getToolRequests(message);
|
||||
|
||||
return toolRequests.map((toolRequest) => (
|
||||
<div key={toolRequest.id} className="goose-message-tool">
|
||||
<ToolCallWithResponse
|
||||
isCancelledMessage={
|
||||
messageIndex < messageHistoryIndex &&
|
||||
toolResponsesMap.get(toolRequest.id) == undefined
|
||||
}
|
||||
toolRequest={toolRequest}
|
||||
toolResponse={toolResponsesMap.get(toolRequest.id)}
|
||||
notifications={toolCallNotifications.get(toolRequest.id)}
|
||||
isStreamingMessage={isStreaming}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-text-muted pt-1 transition-all duration-200 group-hover:-translate-y-4 group-hover:opacity-0">
|
||||
{!isStreaming && timestamp}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../utils';
|
||||
|
||||
export type ToolCallStatus = 'pending' | 'loading' | 'success' | 'error';
|
||||
|
||||
interface ToolCallStatusIndicatorProps {
|
||||
status: ToolCallStatus;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ToolCallStatusIndicator: React.FC<ToolCallStatusIndicatorProps> = ({
|
||||
status,
|
||||
className,
|
||||
}) => {
|
||||
const getStatusStyles = () => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'bg-green-500';
|
||||
case 'error':
|
||||
return 'bg-red-500';
|
||||
case 'loading':
|
||||
return 'bg-yellow-500 animate-pulse';
|
||||
case 'pending':
|
||||
default:
|
||||
return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-background-default',
|
||||
getStatusStyles(),
|
||||
className
|
||||
)}
|
||||
aria-label={`Tool status: ${status}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds a status indicator to a tool icon
|
||||
*/
|
||||
interface ToolIconWithStatusProps {
|
||||
ToolIcon: React.ComponentType<{ className?: string }>;
|
||||
status: ToolCallStatus;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ToolIconWithStatus: React.FC<ToolIconWithStatusProps> = ({
|
||||
ToolIcon,
|
||||
status,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('relative inline-block', className)}>
|
||||
<ToolIcon className="w-3 h-3 flex-shrink-0" />
|
||||
<ToolCallStatusIndicator status={status} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { ToolIconWithStatus, ToolCallStatus } from './ToolCallStatusIndicator';
|
||||
import { getToolCallIcon } from '../utils/toolIconMapping';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { ToolCallArguments, ToolCallArgumentValue } from './ToolCallArguments';
|
||||
import MarkdownContent from './MarkdownContent';
|
||||
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
|
||||
import { cn, snakeToTitleCase } from '../utils';
|
||||
import Dot, { LoadingStatus } from './ui/Dot';
|
||||
import { LoadingStatus } from './ui/Dot';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import { ChevronRight, FlaskConical, LoaderCircle } from 'lucide-react';
|
||||
import { ChevronRight, FlaskConical } from 'lucide-react';
|
||||
import { TooltipWrapper } from './settings/providers/subcomponents/buttons/TooltipWrapper';
|
||||
import MCPUIResourceRenderer from './MCPUIResourceRenderer';
|
||||
import { isUIResource } from '@mcp-ui/client';
|
||||
@@ -173,16 +175,13 @@ function ToolCallView({
|
||||
}: ToolCallViewProps) {
|
||||
const [responseStyle, setResponseStyle] = useState(() => localStorage.getItem('response_style'));
|
||||
|
||||
// Listen for localStorage changes to update the response style
|
||||
useEffect(() => {
|
||||
const handleStorageChange = () => {
|
||||
setResponseStyle(localStorage.getItem('response_style'));
|
||||
};
|
||||
|
||||
// Listen for storage events (changes from other tabs/windows)
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// Listen for custom events (changes from same tab)
|
||||
window.addEventListener('responseStyleChanged', handleStorageChange);
|
||||
|
||||
return () => {
|
||||
@@ -283,12 +282,10 @@ function ToolCallView({
|
||||
const args = toolCall.arguments as Record<string, ToolCallArgumentValue>;
|
||||
const toolName = toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2);
|
||||
|
||||
// Helper function to get string value safely
|
||||
const getStringValue = (value: ToolCallArgumentValue): string => {
|
||||
return typeof value === 'string' ? value : JSON.stringify(value);
|
||||
};
|
||||
|
||||
// Helper function to truncate long values
|
||||
const truncate = (str: string, maxLength: number = 50): string => {
|
||||
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
|
||||
};
|
||||
@@ -442,34 +439,45 @@ function ToolCallView({
|
||||
// Fallback tool name formatting
|
||||
return snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2));
|
||||
};
|
||||
// Map LoadingStatus to ToolCallStatus
|
||||
const getToolCallStatus = (loadingStatus: LoadingStatus): ToolCallStatus => {
|
||||
switch (loadingStatus) {
|
||||
case 'success':
|
||||
return 'success';
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'loading':
|
||||
return 'loading';
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
};
|
||||
|
||||
const toolCallStatus = getToolCallStatus(loadingStatus);
|
||||
|
||||
const toolLabel = (
|
||||
<span className={cn('ml-2', extensionTooltip && 'cursor-pointer hover:opacity-80')}>
|
||||
{getToolLabelContent()}
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-2',
|
||||
extensionTooltip && 'cursor-pointer hover:opacity-80'
|
||||
)}
|
||||
>
|
||||
<ToolIconWithStatus ToolIcon={getToolCallIcon(toolCall.name)} status={toolCallStatus} />
|
||||
<span>{getToolLabelContent()}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<ToolCallExpandable
|
||||
isStartExpanded={isRenderingProgress}
|
||||
isForceExpand={isShouldExpand}
|
||||
label={
|
||||
<>
|
||||
<div className="w-2 flex items-center justify-center">
|
||||
{loadingStatus === 'loading' ? (
|
||||
<LoaderCircle className="animate-spin text-text-muted" size={3} />
|
||||
) : (
|
||||
<Dot size={2} loadingStatus={loadingStatus} />
|
||||
)}
|
||||
</div>
|
||||
{extensionTooltip ? (
|
||||
<TooltipWrapper tooltipContent={extensionTooltip} side="top" align="start">
|
||||
{toolLabel}
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
toolLabel
|
||||
)}
|
||||
</>
|
||||
extensionTooltip ? (
|
||||
<TooltipWrapper tooltipContent={extensionTooltip} side="top" align="start">
|
||||
{toolLabel}
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
toolLabel
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Tool Details */}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export const Archive = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#1E1E20" />
|
||||
<path
|
||||
d="M1.375 6.1875H9.625V8.25C9.625 8.6297 9.3172 8.9375 8.9375 8.9375H2.0625C1.6828 8.9375 1.375 8.6297 1.375 8.25V6.1875Z"
|
||||
fill="#DBD7CE"
|
||||
/>
|
||||
<path d="M1.375 6.1875H9.625V7.5625H1.375V6.1875Z" fill="#434343" />
|
||||
<path d="M1.375 4.8125H9.625V6.1875H1.375V4.8125Z" fill="white" />
|
||||
<path d="M1.375 3.4375H9.625V4.8125H1.375V3.4375Z" fill="#616161" />
|
||||
<path
|
||||
d="M1.375 2.75C1.375 2.3703 1.6828 2.0625 2.0625 2.0625H8.9375C9.3172 2.0625 9.625 2.3703 9.625 2.75V3.4375H1.375V2.75Z"
|
||||
fill="#9F9F9F"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
export const Brain = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#3E3E3E" />
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_782)" />
|
||||
<path d="M5.5 2.0625L1.375 5.5L5.5 8.9375L9.625 5.5L5.5 2.0625Z" fill="#E74786" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_782"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopColor="#323232" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
export const Camera = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_741)" />
|
||||
<path
|
||||
d="M1.375 3.4375C1.375 3.0578 1.6828 2.75 2.0625 2.75H8.9375C9.3172 2.75 9.625 3.0578 9.625 3.4375V7.5625C9.625 7.9422 9.3172 8.25 8.9375 8.25H2.0625C1.6828 8.25 1.375 7.9422 1.375 7.5625V3.4375Z"
|
||||
fill="#2F2F31"
|
||||
/>
|
||||
<path
|
||||
d="M6.53125 5.5C6.53125 6.06954 6.06954 6.53125 5.5 6.53125C4.93046 6.53125 4.46875 6.06954 4.46875 5.5C4.46875 4.93046 4.93046 4.46875 5.5 4.46875C6.06954 4.46875 6.53125 4.93046 6.53125 5.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_741"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#E2E3F7" />
|
||||
<stop offset="1" stopColor="#978E8F" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Code2 = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#1C1C1E" />
|
||||
<rect x="2" y="5" width="7" height="1" rx="0.5" fill="#19FF4F" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
export const Eye = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_805)" />
|
||||
<path
|
||||
d="M1.375 5.5C1.375 4.36091 2.29841 3.4375 3.4375 3.4375H7.5625C8.70159 3.4375 9.625 4.36091 9.625 5.5V5.5C9.625 6.63909 8.70159 7.5625 7.5625 7.5625H3.4375C2.29841 7.5625 1.375 6.63909 1.375 5.5V5.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M6.53125 5.5C6.53125 6.06954 6.06954 6.53125 5.5 6.53125C4.93046 6.53125 4.46875 6.06954 4.46875 5.5C4.46875 4.93046 4.93046 4.46875 5.5 4.46875C6.06954 4.46875 6.53125 4.93046 6.53125 5.5Z"
|
||||
fill="black"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_805"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#00A2FF" />
|
||||
<stop offset="1" stopColor="#5A6DFF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
export const FileEdit = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_755)" />
|
||||
<path
|
||||
d="M8.96875 2.03125L8.96875 4.78125L2.09375 4.78125L2.09375 3.40625C2.09375 2.64686 2.70936 2.03125 3.46875 2.03125L8.96875 2.03125Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M5.03125 6.03125L5.03125 9.03125L2.03125 9.03125L2.03125 7.53125C2.03125 6.70282 2.29988 6.03125 2.63125 6.03125L5.03125 6.03125Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_755"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopColor="#383838" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
export const FilePlus = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_774)" />
|
||||
<rect x="2" y="5" width="7" height="1" rx="0.5" fill="white" />
|
||||
<rect x="6" y="2" width="7" height="1" rx="0.5" transform="rotate(90 6 2)" fill="white" />
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_774"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FF9A00" />
|
||||
<stop offset="1" stopColor="#FFC800" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
export const FileText = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_764)" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8.73614 2.26386C9.00462 2.53235 9.00462 2.96765 8.73614 3.23614L3.23614 8.73614C2.96765 9.00462 2.53235 9.00462 2.26386 8.73614C1.99538 8.46765 1.99538 8.03235 2.26386 7.76386L7.76386 2.26386C8.03235 1.99538 8.46765 1.99538 8.73614 2.26386Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_764"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FFB645" />
|
||||
<stop offset="1" stopColor="#FF8735" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
export const Globe = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clipPath="url(#clip0_6313_720)">
|
||||
<rect width="11" height="11" rx="2" fill="white" />
|
||||
<path
|
||||
d="M10.3125 5.5C10.3125 8.15787 8.15787 10.3125 5.5 10.3125C2.84213 10.3125 0.6875 8.15787 0.6875 5.5C0.6875 2.84213 2.84213 0.6875 5.5 0.6875C8.15787 0.6875 10.3125 2.84213 10.3125 5.5Z"
|
||||
fill="url(#paint0_linear_6313_720)"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_720"
|
||||
x1="5.5"
|
||||
y1="0.6875"
|
||||
x2="5.5"
|
||||
y2="10.3125"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#00CAF7" />
|
||||
<stop offset="1" stopColor="#0B54DE" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6313_720">
|
||||
<rect width="11" height="11" rx="2" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
export const Harddrive = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#1E1E20" />
|
||||
<path
|
||||
d="M5 3.5C5 4.32843 4.32843 5 3.5 5C2.67157 5 2 4.32843 2 3.5C2 2.67157 2.67157 2 3.5 2C4.32843 2 5 2.67157 5 3.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
export const Monitor = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="white" />
|
||||
<path
|
||||
d="M9.625 5.5C9.625 7.77817 7.77817 9.625 5.5 9.625C3.22183 9.625 1.375 7.77817 1.375 5.5C1.375 3.22183 3.22183 1.375 5.5 1.375C7.77817 1.375 9.625 3.22183 9.625 5.5Z"
|
||||
fill="#E60023"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
export const Numbers = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_717)" />
|
||||
<path
|
||||
d="M4.8125 3.4375C4.8125 3.0578 5.1203 2.75 5.5 2.75C5.8797 2.75 6.1875 3.0578 6.1875 3.4375V7.5625C6.1875 7.9422 5.8797 8.25 5.5 8.25C5.1203 8.25 4.8125 7.9422 4.8125 7.5625V3.4375Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M2.0625 6.1875C2.0625 5.8078 2.3703 5.5 2.75 5.5C3.1297 5.5 3.4375 5.8078 3.4375 6.1875V7.5625C3.4375 7.9422 3.1297 8.25 2.75 8.25C2.3703 8.25 2.0625 7.9422 2.0625 7.5625V6.1875Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M8.25 4.125C7.8703 4.125 7.5625 4.4328 7.5625 4.8125V7.5625C7.5625 7.9422 7.8703 8.25 8.25 8.25C8.6297 8.25 8.9375 7.9422 8.9375 7.5625V4.8125C8.9375 4.4328 8.6297 4.125 8.25 4.125Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_717"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#73FA80" />
|
||||
<stop offset="1" stopColor="#00D648" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Save = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#C32361" />
|
||||
<path d="M5.5 2.0625L1.375 5.5L5.5 8.9375L9.625 5.5L5.5 2.0625Z" fill="#E74786" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
export const Search = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_760)" />
|
||||
<path
|
||||
d="M7.5625 5.5C7.5625 6.63909 6.63909 7.5625 5.5 7.5625C4.36091 7.5625 3.4375 6.63909 3.4375 5.5C3.4375 4.36091 4.36091 3.4375 5.5 3.4375C6.63909 3.4375 7.5625 4.36091 7.5625 5.5Z"
|
||||
fill="#2D2D2E"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_760"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#D2D5DA" />
|
||||
<stop offset="1" stopColor="#8B8E95" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
export const Settings = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g clipPath="url(#clip0_6313_731)">
|
||||
<rect width="11" height="11" rx="2" fill="url(#paint0_linear_6313_731)" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.5 8.9375C7.39848 8.9375 8.9375 7.39848 8.9375 5.5C8.9375 3.60152 7.39848 2.0625 5.5 2.0625C3.60152 2.0625 2.0625 3.60152 2.0625 5.5C2.0625 7.39848 3.60152 8.9375 5.5 8.9375ZM5.5 10.3125C8.15787 10.3125 10.3125 8.15787 10.3125 5.5C10.3125 2.84213 8.15787 0.6875 5.5 0.6875C2.84213 0.6875 0.6875 2.84213 0.6875 5.5C0.6875 8.15787 2.84213 10.3125 5.5 10.3125Z"
|
||||
fill="#2D2D2E"
|
||||
/>
|
||||
<path
|
||||
d="M7.5625 5.5C7.5625 6.63909 6.63909 7.5625 5.5 7.5625C4.36091 7.5625 3.4375 6.63909 3.4375 5.5C3.4375 4.36091 4.36091 3.4375 5.5 3.4375C6.63909 3.4375 7.5625 4.36091 7.5625 5.5Z"
|
||||
fill="#2D2D2E"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6313_731"
|
||||
x1="5.5"
|
||||
y1="0"
|
||||
x2="5.5"
|
||||
y2="11"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#D2D5DA" />
|
||||
<stop offset="1" stopColor="#8B8E95" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6313_731">
|
||||
<rect width="11" height="11" rx="2" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Terminal = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#1C1C1E" />
|
||||
<path d="M2 3L2 2L6 2L6 3L2 3Z" fill="#19FF4F" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Tool = ({ className }: { className?: string }) => (
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 11 11"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<rect width="11" height="11" rx="2" fill="#1E1E20" />
|
||||
<rect x="2" y="5" width="7" height="1" rx="0.5" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
export { Archive } from './Archive';
|
||||
export { Brain } from './Brain';
|
||||
export { Camera } from './Camera';
|
||||
export { Code2 } from './Code2';
|
||||
export { Eye } from './Eye';
|
||||
export { FileEdit } from './FileEdit';
|
||||
export { FilePlus } from './FilePlus';
|
||||
export { FileText } from './FileText';
|
||||
export { Globe } from './Globe';
|
||||
export { Harddrive } from './Harddrive';
|
||||
export { Monitor } from './Monitor';
|
||||
export { Numbers } from './Numbers';
|
||||
export { Save } from './Save';
|
||||
export { Search } from './Search';
|
||||
export { Settings } from './Settings';
|
||||
export { Terminal } from './Terminal';
|
||||
export { Tool } from './Tool';
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Message, getToolRequests, getTextContent, getToolResponses } from '../types/message';
|
||||
|
||||
export function identifyConsecutiveToolCalls(messages: Message[]): number[][] {
|
||||
const chains: number[][] = [];
|
||||
let currentChain: number[] = [];
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const message = messages[i];
|
||||
const toolRequests = getToolRequests(message);
|
||||
const toolResponses = getToolResponses(message);
|
||||
const textContent = getTextContent(message);
|
||||
const hasText = textContent.trim().length > 0;
|
||||
|
||||
if (toolResponses.length > 0 && toolRequests.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toolRequests.length > 0) {
|
||||
if (hasText) {
|
||||
if (currentChain.length > 0) {
|
||||
if (currentChain.length > 1) {
|
||||
chains.push([...currentChain]);
|
||||
}
|
||||
}
|
||||
currentChain = [i];
|
||||
} else {
|
||||
currentChain.push(i);
|
||||
}
|
||||
} else if (hasText) {
|
||||
if (currentChain.length > 1) {
|
||||
chains.push([...currentChain]);
|
||||
}
|
||||
currentChain = [];
|
||||
} else {
|
||||
if (currentChain.length > 1) {
|
||||
chains.push([...currentChain]);
|
||||
}
|
||||
currentChain = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChain.length > 1) {
|
||||
chains.push(currentChain);
|
||||
}
|
||||
|
||||
return chains;
|
||||
}
|
||||
|
||||
export function shouldHideMessage(messageIndex: number, chains: number[][]): boolean {
|
||||
for (const chain of chains) {
|
||||
if (chain.includes(messageIndex)) {
|
||||
return chain[0] !== messageIndex;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getChainForMessage(messageIndex: number, chains: number[][]): number[] | null {
|
||||
return chains.find(chain => chain.includes(messageIndex)) || null;
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Archive,
|
||||
Brain,
|
||||
Camera,
|
||||
Code2,
|
||||
Eye,
|
||||
FileEdit,
|
||||
FilePlus,
|
||||
FileText,
|
||||
Globe,
|
||||
Monitor,
|
||||
Numbers,
|
||||
Save,
|
||||
Search,
|
||||
Settings,
|
||||
Terminal,
|
||||
Tool,
|
||||
} from '../components/icons/toolcalls';
|
||||
|
||||
export type ToolIconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps tool names to their corresponding icon components
|
||||
* @param toolName - The name of the tool (extracted from toolCall.name)
|
||||
* @returns React component for the tool icon
|
||||
*/
|
||||
export const getToolIcon = (toolName: string): React.ComponentType<ToolIconProps> => {
|
||||
switch (toolName) {
|
||||
// Developer Extension Tools
|
||||
case 'text_editor':
|
||||
return FileEdit;
|
||||
case 'shell':
|
||||
return Terminal;
|
||||
|
||||
// Memory Extension Tools
|
||||
case 'remember_memory':
|
||||
return Save;
|
||||
case 'retrieve_memories':
|
||||
return Brain;
|
||||
|
||||
// Computer Controller Extension Tools
|
||||
case 'automation_script':
|
||||
return Settings;
|
||||
case 'computer_control':
|
||||
return Monitor;
|
||||
case 'web_scrape':
|
||||
return Globe;
|
||||
case 'screen_capture':
|
||||
return Camera;
|
||||
case 'pdf_tool':
|
||||
return FileText;
|
||||
case 'docx_tool':
|
||||
return FileText;
|
||||
case 'xlsx_tool':
|
||||
return Numbers;
|
||||
case 'cache':
|
||||
return Archive;
|
||||
|
||||
// File Operations
|
||||
case 'search':
|
||||
return Search;
|
||||
case 'read':
|
||||
return Eye;
|
||||
case 'create_file':
|
||||
return FilePlus;
|
||||
case 'update_file':
|
||||
return FileEdit;
|
||||
|
||||
// Google Workspace Tools (if still supported)
|
||||
case 'sheets_tool':
|
||||
return Numbers;
|
||||
case 'docs_tool':
|
||||
return FileText;
|
||||
|
||||
// Special Tools
|
||||
case 'final_output':
|
||||
return Tool; // Could be a checkmark icon if we had one
|
||||
|
||||
// Default fallback for unknown tools
|
||||
default:
|
||||
return Tool;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps extension names to their corresponding icon components
|
||||
* @param extensionName - The name of the extension
|
||||
* @returns React component for the extension icon
|
||||
*/
|
||||
export const getExtensionIcon = (extensionName: string): React.ComponentType<ToolIconProps> => {
|
||||
switch (extensionName) {
|
||||
case 'developer':
|
||||
return Code2;
|
||||
case 'memory':
|
||||
return Brain;
|
||||
case 'computercontroller':
|
||||
return Monitor;
|
||||
default:
|
||||
return Tool;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to extract tool name from full tool call name
|
||||
* @param toolCallName - Full tool call name (e.g., "developer__text_editor")
|
||||
* @returns Extracted tool name (e.g., "text_editor")
|
||||
*/
|
||||
export const extractToolName = (toolCallName: string): string => {
|
||||
return toolCallName.substring(toolCallName.lastIndexOf('__') + 2);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to extract extension name from full tool call name
|
||||
* @param toolCallName - Full tool call name (e.g., "developer__text_editor")
|
||||
* @returns Extracted extension name (e.g., "developer")
|
||||
*/
|
||||
export const extractExtensionName = (toolCallName: string): string => {
|
||||
const lastIndex = toolCallName.lastIndexOf('__');
|
||||
return lastIndex === -1 ? '' : toolCallName.substring(0, lastIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to get the appropriate icon for a tool call
|
||||
* @param toolCallName - Full tool call name (e.g., "developer__text_editor")
|
||||
* @param useExtensionIcon - Whether to use extension icon instead of tool icon
|
||||
* @returns React component for the icon
|
||||
*/
|
||||
export const getToolCallIcon = (
|
||||
toolCallName: string,
|
||||
useExtensionIcon: boolean = false
|
||||
): React.ComponentType<ToolIconProps> => {
|
||||
if (useExtensionIcon) {
|
||||
const extensionName = extractExtensionName(toolCallName);
|
||||
return getExtensionIcon(extensionName);
|
||||
}
|
||||
|
||||
const toolName = extractToolName(toolCallName);
|
||||
return getToolIcon(toolName);
|
||||
};
|
||||
Reference in New Issue
Block a user