ghostdrive1's picture
Upload folder using huggingface_hub
0b9dc2e verified
Raw
History Blame Contribute Delete
4.34 kB
import type { ToolCallBlock, ToolResultBlock } from '@agentscope-ai/agentscope/message';
import { ChevronRight } from 'lucide-react';
import * as mime from 'mime-types';
import type { ReactNode } from 'react';
import { CornerLine, ToolStateIcon } from './_shared';
import type { TFunction, ToolCallWithResult } from './types';
import { Button } from '@/components/ui/button.tsx';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible.tsx';
function processToolInput(input: string): string {
try {
const obj = JSON.parse(input);
const entries = Object.entries(obj).map(([k, v]) => `${k}: "${v}"`);
return entries.join('\n');
} catch {
return input;
}
}
export function defaultGetDisplayName(call: ToolCallBlock): string {
return call.name;
}
export function defaultRenderCallArgs(call: ToolCallBlock): ReactNode {
if (call.input.length <= 2) return null;
return processToolInput(call.input);
}
export function defaultRenderResult(
call: ToolCallBlock,
result: ToolResultBlock,
t: TFunction,
maxLines = 7,
): ReactNode {
if (call.state === 'asking' || !result || result.state === 'running') {
return <span>{t('common.running')} ...</span>;
}
if (result.state === 'interrupted') {
return <span>{t('common.interrupted')}</span>;
}
let resultStr: string;
if (typeof result.output === 'string') {
resultStr = result.output;
} else {
const parts = result.output.map((b) => {
if (b.type === 'text') return b.text;
const mainType = b.source.media_type.split('/')[0].toUpperCase();
const ext = (mime.extension(b.source.media_type) || 'bin').toLowerCase();
return `[${mainType}.${ext}]`;
});
resultStr = parts.join('\n');
}
let lines = resultStr.split('\n');
if (lines.length > maxLines) {
const total = lines.length;
lines = lines.slice(0, maxLines);
lines.push(t('tool.moreLines', { count: total - maxLines }));
}
return (
<div className="flex flex-col flex-1 min-w-0">
{lines.map((line, i) => (
<div key={i} className="truncate">
{line}
</div>
))}
</div>
);
}
export function defaultRenderConfirmBody(call: ToolCallBlock): ReactNode {
return (
<div className="w-full max-w-full overflow-hidden text-ellipsis truncate">
<div className="text-secondary-foreground">{call.input}</div>
</div>
);
}
/**
* Default group layout: each call is an independent block with a state icon,
* `displayName(args)` header line, and (optionally) a corner-line-prefixed
* result block beneath it. Used by tools without a custom `renderGroup`,
* e.g. arbitrary MCP tools.
*
* `getDisplayName` / `renderCallArgs` / `renderResult` are passed in from
* `index.ts` so this function stays decoupled from the renderer registry.
*/
export function defaultRenderGroup(
calls: ToolCallWithResult[],
_t: TFunction,
resolvers: {
getDisplayName: (call: ToolCallBlock) => string;
renderCallArgs: (call: ToolCallBlock) => ReactNode;
renderResult: (call: ToolCallBlock, result: ToolResultBlock) => ReactNode;
},
): ReactNode {
return (
<>
{calls.map(({ call, result }) => {
const displayName = resolvers.getDisplayName(call);
const args = resolvers.renderCallArgs(call);
const resultContent = result ? resolvers.renderResult(call, result) : null;
return (
<Collapsible key={call.id} className="flex flex-col w-full max-w-full text-sm">
<CollapsibleTrigger asChild>
<Button
variant={'ghost'}
className="group hover:bg-transparent data-[state=open]:bg-transparent justify-start px-0 gap-2 active:!translate-y-0"
>
<ToolStateIcon states={[result?.state]} />
<strong className="shrink-0 text-primary">{displayName}</strong>
{args && <span className="truncate min-w-0 text-left">{args}</span>}
{resultContent && (
<ChevronRight className="size-3 shrink-0 group-data-[state=open]:rotate-90" />
)}
</Button>
</CollapsibleTrigger>
{resultContent && (
<CollapsibleContent className="flex flex-row gap-x-2 pl-6 max-w-full">
<CornerLine />
{resultContent}
</CollapsibleContent>
)}
</Collapsible>
);
})}
</>
);
}