66a8b7f597
* 补全完整的工具参数 * 调整提示词应对一些bad case * 把attachment的名称/描述纳入上下文 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/101 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
161 lines
6.2 KiB
TypeScript
161 lines
6.2 KiB
TypeScript
import { Check, Coins, Loader2, Pencil, X } from 'lucide-react';
|
||
|
||
import type { EditorAgentToolCall } from '@/packages/shared/src/contracts';
|
||
import { editorAgentToolLabel } from '@/src/components/image-editor/EditorAgentConversation/toolCallPresentation.ts';
|
||
import { getEditorGenerationModelDisplayName } from '@/src/components/image-editor/ImageCanvasGenerationModel.ts';
|
||
import { ResolvedAssetImage } from '@/src/components/ResolvedAssetImage.tsx';
|
||
|
||
type PendingToolCallAction = 'confirm' | 'cancel' | null;
|
||
|
||
type PendingToolCallProps = {
|
||
messageId: number;
|
||
toolCall: EditorAgentToolCall;
|
||
busyAction: PendingToolCallAction;
|
||
onConfirm: (messageId: number) => Promise<void>;
|
||
onCancel: (messageId: number) => Promise<void>;
|
||
};
|
||
|
||
function readString(value: unknown) {
|
||
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
||
}
|
||
|
||
function displayStringArgument(name: string, value: string) {
|
||
return name === 'model' ? getEditorGenerationModelDisplayName(value) : value;
|
||
}
|
||
|
||
export function PendingToolCall({
|
||
messageId,
|
||
toolCall,
|
||
busyAction,
|
||
onConfirm,
|
||
onCancel,
|
||
}: PendingToolCallProps) {
|
||
const displayArgs = toolCall.displayArgs;
|
||
const label = editorAgentToolLabel(toolCall.toolName);
|
||
const isBusy = busyAction !== null;
|
||
const statusLabel =
|
||
busyAction === 'confirm'
|
||
? '执行中'
|
||
: busyAction === 'cancel'
|
||
? '取消中'
|
||
: '待确认';
|
||
|
||
const handleCancel = () => {
|
||
void onCancel(messageId).catch(() => undefined);
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
void onConfirm(messageId).catch(() => undefined);
|
||
};
|
||
|
||
return (
|
||
<article className="flex justify-start" aria-label={`待确认的${label}操作`}>
|
||
<div className="w-full max-w-[86%] rounded-lg border border-slate-200 bg-white p-3 text-sm text-slate-700 shadow-sm">
|
||
<div className="flex items-center gap-2 font-medium text-slate-900">
|
||
<Pencil className="h-4 w-4 shrink-0" aria-hidden="true" />
|
||
<span>{label}</span>
|
||
<span className="ml-auto text-xs font-normal text-amber-700">
|
||
{statusLabel}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="mt-3 space-y-3 empty:hidden">
|
||
{displayArgs.stringArgs.map((argument, index) => (
|
||
<div
|
||
key={`${argument.name}-${index}`}
|
||
className="rounded-lg bg-slate-50 px-2.5 py-2"
|
||
>
|
||
<div className="text-xs font-medium text-slate-500">
|
||
{argument.label}
|
||
</div>
|
||
<div className="mt-1 whitespace-pre-wrap break-words text-sm leading-5 text-slate-800">
|
||
{displayStringArgument(argument.name, argument.value)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{displayArgs.imageArgs.map((argument, argumentIndex) => (
|
||
<div key={`${argument.name}-${argumentIndex}`}>
|
||
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
|
||
<span>{argument.label}</span>
|
||
<span className="font-normal text-slate-400">
|
||
{argument.refs.length} 张
|
||
</span>
|
||
</div>
|
||
{argument.refs.length ? (
|
||
<div className="mt-1.5 flex flex-wrap gap-2">
|
||
{argument.refs.map((image, imageIndex) => {
|
||
const imageLabel =
|
||
readString(image.label) ?? `图片 ${imageIndex + 1}`;
|
||
return (
|
||
<figure
|
||
key={`${image.imageId}-${imageIndex}`}
|
||
className="w-20 min-w-0"
|
||
>
|
||
<div className="overflow-hidden rounded-lg border border-slate-200 bg-slate-100">
|
||
<ResolvedAssetImage
|
||
src={image.thumbnailSrc ?? image.imageSrc}
|
||
objectKey={image.objectKey}
|
||
refreshKey={image.imageId}
|
||
alt={`${argument.label}:${imageLabel}`}
|
||
className="h-20 w-20 object-contain"
|
||
/>
|
||
</div>
|
||
{image.label ? (
|
||
<figcaption className="mt-1 truncate text-center text-[11px] text-slate-500">
|
||
{image.label}
|
||
</figcaption>
|
||
) : null}
|
||
</figure>
|
||
);
|
||
})}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-3 flex items-center gap-1.5 rounded-lg border border-amber-200 bg-amber-50 px-2.5 py-2 text-xs font-medium text-amber-800">
|
||
<Coins className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
|
||
<span>预计消耗 {displayArgs.extras.priceMudPoints}泥点</span>
|
||
</div>
|
||
|
||
<div className="mt-3 flex justify-end gap-2">
|
||
<button
|
||
type="button"
|
||
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-slate-200 bg-white px-3 text-sm text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||
disabled={isBusy}
|
||
onClick={handleCancel}
|
||
>
|
||
{busyAction === 'cancel' ? (
|
||
<Loader2
|
||
className="h-3.5 w-3.5 animate-spin"
|
||
aria-hidden="true"
|
||
/>
|
||
) : (
|
||
<X className="h-3.5 w-3.5" aria-hidden="true" />
|
||
)}
|
||
{busyAction === 'cancel' ? '取消中' : '取消'}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md bg-slate-900 px-3 text-sm font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
|
||
disabled={isBusy}
|
||
onClick={handleConfirm}
|
||
>
|
||
{busyAction === 'confirm' ? (
|
||
<Loader2
|
||
className="h-3.5 w-3.5 animate-spin"
|
||
aria-hidden="true"
|
||
/>
|
||
) : (
|
||
<Check className="h-3.5 w-3.5" aria-hidden="true" />
|
||
)}
|
||
{busyAction === 'confirm' ? '执行中' : '确认'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|