mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-22 15:54:29 +01:00
158 lines
4.7 KiB
TypeScript
158 lines
4.7 KiB
TypeScript
import React from 'react';
|
|
import { Card } from './ui/card';
|
|
import Box from './ui/Box';
|
|
import { ToolCallArguments } from './ToolCallArguments';
|
|
import MarkdownContent from './MarkdownContent';
|
|
import { LoadingPlaceholder } from './LoadingPlaceholder';
|
|
import { ChevronUp } from 'lucide-react';
|
|
import { Content, ToolRequestMessageContent, ToolResponseMessageContent } from '../types/message';
|
|
import { snakeToTitleCase } from '../utils';
|
|
|
|
interface ToolCallWithResponseProps {
|
|
isCancelledMessage: boolean;
|
|
toolRequest: ToolRequestMessageContent;
|
|
toolResponse?: ToolResponseMessageContent;
|
|
}
|
|
|
|
export default function ToolCallWithResponse({
|
|
isCancelledMessage,
|
|
toolRequest,
|
|
toolResponse,
|
|
}: ToolCallWithResponseProps) {
|
|
const toolCall = toolRequest.toolCall.status === 'success' ? toolRequest.toolCall.value : null;
|
|
|
|
if (!toolCall) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<Card className="">
|
|
<ToolCallView toolCall={toolCall} />
|
|
{!isCancelledMessage ? (
|
|
toolResponse ? (
|
|
<ToolResultView
|
|
result={
|
|
toolResponse.toolResult.status === 'success'
|
|
? toolResponse.toolResult.value
|
|
: undefined
|
|
}
|
|
/>
|
|
) : (
|
|
<LoadingPlaceholder />
|
|
)
|
|
) : undefined}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ToolCallViewProps {
|
|
toolCall: {
|
|
name: string;
|
|
arguments: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
function ToolCallView({ toolCall }: ToolCallViewProps) {
|
|
return (
|
|
<div>
|
|
<div className="flex items-center mb-4">
|
|
<Box size={16} />
|
|
<span className="ml-[8px] text-textStandard">
|
|
{snakeToTitleCase(toolCall.name.substring(toolCall.name.lastIndexOf('__') + 2))}
|
|
</span>
|
|
</div>
|
|
|
|
{toolCall.arguments && <ToolCallArguments args={toolCall.arguments} />}
|
|
|
|
<div className="self-stretch h-px my-[10px] -mx-4 bg-borderSubtle dark:bg-gray-700" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface ToolResultViewProps {
|
|
result?: Content[];
|
|
}
|
|
|
|
function ToolResultView({ result }: ToolResultViewProps) {
|
|
// State to track expanded items
|
|
const [expandedItems, setExpandedItems] = React.useState<number[]>([]);
|
|
|
|
// If no result info, don't show anything
|
|
if (!result) return null;
|
|
|
|
// Find results where either audience is not set, or it's set to a list that includes user
|
|
const filteredResults = result.filter((item) => {
|
|
// Check audience (which may not be in the type)
|
|
const audience = item.annotations?.audience;
|
|
|
|
return !audience || audience.includes('user');
|
|
});
|
|
|
|
if (filteredResults.length === 0) return null;
|
|
|
|
const toggleExpand = (index: number) => {
|
|
setExpandedItems((prev) =>
|
|
prev.includes(index) ? prev.filter((i) => i !== index) : [...prev, index]
|
|
);
|
|
};
|
|
|
|
const shouldShowExpanded = (item: Content, index: number) => {
|
|
return (
|
|
(item.annotations &&
|
|
item.annotations.priority !== undefined &&
|
|
item.annotations.priority >= 0.5) ||
|
|
expandedItems.includes(index)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="">
|
|
{filteredResults.map((item, index) => {
|
|
const isExpanded = shouldShowExpanded(item, index);
|
|
const shouldMinimize =
|
|
!item.annotations ||
|
|
item.annotations.priority === undefined ||
|
|
item.annotations.priority < 0.5;
|
|
return (
|
|
<div key={index} className="relative">
|
|
{shouldMinimize && (
|
|
<button
|
|
onClick={() => toggleExpand(index)}
|
|
className="mb-1 flex items-center text-textStandard"
|
|
>
|
|
<span className="mr-2 text-sm">Output</span>
|
|
<ChevronUp
|
|
className={`h-5 w-5 transition-all origin-center ${!isExpanded ? 'rotate-180' : ''}`}
|
|
/>
|
|
</button>
|
|
)}
|
|
{(isExpanded || !shouldMinimize) && (
|
|
<>
|
|
{item.type === 'text' && item.text && (
|
|
<MarkdownContent
|
|
content={item.text}
|
|
className="whitespace-pre-wrap p-2 max-w-full overflow-x-auto"
|
|
/>
|
|
)}
|
|
{item.type === 'image' && (
|
|
<img
|
|
src={`data:${item.mimeType};base64,${item.data}`}
|
|
alt="Tool result"
|
|
className="max-w-full h-auto rounded-md my-2"
|
|
onError={(e) => {
|
|
console.error('Failed to load image: Invalid MIME-type encoded image data');
|
|
e.currentTarget.style.display = 'none';
|
|
}}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|