f5368c825f
重构了editor agent. 使得它能进行多轮工具调用, 把工具调用的结果嵌入到上下文里。 对于上下文的图片, 使用哈希后的id用来引用,不暴露细节信息to llm。 当前实现, 状态直接维护在json doc里, 需要对整个doc加锁,任务目前只能串行。 把SSE改成一个简单请求( 因为生图工具耗时需要二次确认, 不需要再实时展示给用户进度)。 增加确认/取消生图操作。 把tool的参数和错误情况做了反馈。 TODO: 工具调用的规范/prompt还可以改进。 改用external job来做素材生成, 针对来自editor agent 的生成会把生成资产的引用加入到结果json里, 客户端轮询 external job 确定生成状态, 发现结束了或者失败了就 重新get 会话,后端重新提供会话的时候把 结果插入回会话历史里,用来让 ai 引用 以及显示 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/76 Reviewed-by: 段舒康 <kdletters@qq.com> Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 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 { 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;
|
||
}
|
||
|
||
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">
|
||
{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>
|
||
);
|
||
}
|