Files
Genarrative/src/components/image-editor/EditorAgentConversation/PendingToolCall.tsx
T
k88936 66a8b7f597
Project CI / Repository checks (push) Successful in 53s
Project CI / Frontend tests (push) Successful in 2m34s
Project CI / Native shell tests (push) Successful in 2m42s
Project CI / Backend tests (push) Successful in 3m28s
画布agent 完善上下文 (#101)
* 补全完整的工具参数
* 调整提示词应对一些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>
2026-07-28 15:44:05 +08:00

161 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}