654 lines
22 KiB
TypeScript
654 lines
22 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
CheckCircle2,
|
||
ChevronDown,
|
||
HelpCircle,
|
||
ListChecks,
|
||
ListTree,
|
||
LockKeyhole,
|
||
ShieldCheck,
|
||
XCircle,
|
||
type LucideIcon
|
||
} from "lucide-react";
|
||
import { AnimatePresence, motion } from "motion/react";
|
||
import { useState, type ReactNode } from "react";
|
||
import { cn } from "@/lib/utils";
|
||
import { Button } from "@/shared/ui/button";
|
||
import {
|
||
agentLayoutTransition,
|
||
agentPanelItemVariants,
|
||
agentPanelListVariants,
|
||
agentPanelSectionVariants
|
||
} from "./agent-motion";
|
||
import type {
|
||
AgentChatMessage,
|
||
AgentChatProgress,
|
||
AgentPermissionReply,
|
||
AgentPermissionRequest,
|
||
AgentQuestionInfo,
|
||
AgentQuestionRequest,
|
||
AgentTodoUpdate
|
||
} from "../types";
|
||
|
||
type ProgressStatus = AgentChatProgress["status"];
|
||
type TodoStatus = AgentTodoUpdate["todos"][number]["status"];
|
||
type PermissionStatus = AgentPermissionRequest["status"];
|
||
type QuestionStatus = AgentQuestionRequest["status"];
|
||
|
||
const progressStatusMeta: Record<ProgressStatus, { label: string; className: string; dotClassName: string }> = {
|
||
running: {
|
||
label: "进行中",
|
||
className: "bg-blue-100 text-blue-700",
|
||
dotClassName: "border-blue-500 bg-blue-100"
|
||
},
|
||
completed: {
|
||
label: "完成",
|
||
className: "bg-emerald-100 text-emerald-700",
|
||
dotClassName: "border-emerald-500 bg-emerald-100"
|
||
},
|
||
error: {
|
||
label: "失败",
|
||
className: "bg-red-100 text-red-700",
|
||
dotClassName: "border-red-500 bg-red-100"
|
||
}
|
||
};
|
||
|
||
const todoStatusMeta: Record<TodoStatus, { label: string; className: string; dotClassName: string }> = {
|
||
completed: {
|
||
label: "完成",
|
||
className: "bg-emerald-100 text-emerald-700",
|
||
dotClassName: "border-emerald-500 bg-emerald-500"
|
||
},
|
||
in_progress: {
|
||
label: "进行中",
|
||
className: "bg-blue-100 text-blue-700",
|
||
dotClassName: "border-blue-500 bg-blue-100"
|
||
},
|
||
pending: {
|
||
label: "待办",
|
||
className: "bg-slate-100 text-slate-600",
|
||
dotClassName: "border-slate-300 bg-white"
|
||
},
|
||
cancelled: {
|
||
label: "取消",
|
||
className: "bg-slate-200 text-slate-500",
|
||
dotClassName: "border-slate-300 bg-slate-200"
|
||
}
|
||
};
|
||
|
||
const permissionStatusMeta: Record<PermissionStatus, { label: string; className: string }> = {
|
||
approved_always: { label: "已始终允许", className: "bg-emerald-100 text-emerald-700" },
|
||
approved_once: { label: "已允许一次", className: "bg-emerald-100 text-emerald-700" },
|
||
rejected: { label: "已拒绝", className: "bg-red-100 text-red-700" },
|
||
aborted: { label: "已中止", className: "bg-slate-200 text-slate-500" },
|
||
error: { label: "失败", className: "bg-red-100 text-red-700" },
|
||
submitting: { label: "提交中", className: "bg-blue-100 text-blue-700" },
|
||
pending: { label: "待确认", className: "bg-orange-100 text-orange-700" }
|
||
};
|
||
|
||
const questionStatusMeta: Record<QuestionStatus, { label: string; className: string }> = {
|
||
answered: { label: "已回答", className: "bg-emerald-100 text-emerald-700" },
|
||
rejected: { label: "已跳过", className: "bg-slate-200 text-slate-500" },
|
||
error: { label: "失败", className: "bg-red-100 text-red-700" },
|
||
submitting: { label: "提交中", className: "bg-blue-100 text-blue-700" },
|
||
pending: { label: "待回答", className: "bg-blue-100 text-blue-700" }
|
||
};
|
||
|
||
export function AgentMessageDetails({
|
||
message,
|
||
onReplyPermission,
|
||
onReplyQuestion,
|
||
onRejectQuestion
|
||
}: {
|
||
message: AgentChatMessage;
|
||
onReplyPermission?: (permission: AgentPermissionRequest, reply: AgentPermissionReply) => Promise<void> | void;
|
||
onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise<void> | void;
|
||
onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
|
||
}) {
|
||
const hasDetails = Boolean(
|
||
message.todos?.todos.length ||
|
||
message.permissions?.length ||
|
||
message.questions?.length
|
||
);
|
||
|
||
if (!hasDetails) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<motion.div className="space-y-2" layout variants={agentPanelListVariants} animate="animate">
|
||
<AnimatePresence initial={false} mode="popLayout">
|
||
{message.todos ? (
|
||
<motion.div key="todos" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
|
||
<TodoCard todoUpdate={message.todos} />
|
||
</motion.div>
|
||
) : null}
|
||
{message.permissions?.length ? (
|
||
<motion.div key="permissions" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
|
||
<PermissionList permissions={message.permissions} onReplyPermission={onReplyPermission} />
|
||
</motion.div>
|
||
) : null}
|
||
{message.questions?.length ? (
|
||
<motion.div key="questions" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
|
||
<QuestionList
|
||
questions={message.questions}
|
||
onReplyQuestion={onReplyQuestion}
|
||
onRejectQuestion={onRejectQuestion}
|
||
/>
|
||
</motion.div>
|
||
) : null}
|
||
</AnimatePresence>
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
export function AgentMessageProgress({
|
||
progress,
|
||
running
|
||
}: {
|
||
progress?: AgentChatProgress[];
|
||
running: boolean;
|
||
}) {
|
||
if (!progress?.length) {
|
||
return null;
|
||
}
|
||
|
||
return <ProgressList progress={progress} running={running} />;
|
||
}
|
||
|
||
function AgentNestedBlock({
|
||
icon: Icon,
|
||
iconClassName,
|
||
title,
|
||
children
|
||
}: {
|
||
icon: LucideIcon;
|
||
iconClassName: string;
|
||
title: string;
|
||
children: ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="agent-panel-nested rounded-xl p-2.5">
|
||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-600">
|
||
<Icon size={14} className={iconClassName} aria-hidden="true" />
|
||
{title}
|
||
</div>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function AnimatedDetailItem({ children }: { children: ReactNode }) {
|
||
return (
|
||
<motion.div
|
||
layout
|
||
initial="initial"
|
||
animate="animate"
|
||
exit="exit"
|
||
variants={agentPanelItemVariants}
|
||
transition={agentLayoutTransition}
|
||
>
|
||
{children}
|
||
</motion.div>
|
||
);
|
||
}
|
||
|
||
function ProgressList({ progress, running }: { progress: AgentChatProgress[]; running: boolean }) {
|
||
const [expanded, setExpanded] = useState(false);
|
||
const recentProgress = progress.slice(-3);
|
||
const visibleProgress = expanded ? progress : running ? recentProgress : [];
|
||
|
||
return (
|
||
<div className="agent-panel-nested rounded-xl p-2.5">
|
||
<button
|
||
type="button"
|
||
className="flex w-full items-center gap-2 text-left text-xs font-semibold text-slate-600"
|
||
aria-expanded={expanded}
|
||
onClick={() => setExpanded((current) => !current)}
|
||
>
|
||
<ListTree size={16} className="shrink-0 text-blue-700" aria-hidden="true" />
|
||
<span>执行进度</span>
|
||
<span className="flex-1" />
|
||
<span className="shrink-0 font-normal text-slate-400">
|
||
{expanded
|
||
? `全部 ${progress.length} 条`
|
||
: running
|
||
? `最近 ${recentProgress.length} 条`
|
||
: `共 ${progress.length} 条`}
|
||
</span>
|
||
<ChevronDown
|
||
size={14}
|
||
className={cn("shrink-0 text-slate-400 transition-transform", expanded && "rotate-180")}
|
||
aria-hidden="true"
|
||
/>
|
||
</button>
|
||
<AnimatePresence initial={false}>
|
||
{visibleProgress.length ? (
|
||
<motion.ol
|
||
className="mt-2 space-y-1.5 overflow-hidden"
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: "auto", opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
transition={agentLayoutTransition}
|
||
>
|
||
{visibleProgress.map((item) => (
|
||
<li key={item.id} className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs">
|
||
<span
|
||
className={cn(
|
||
"mt-0.5 h-3.5 w-3.5 rounded-full border",
|
||
progressStatusMeta[item.status].dotClassName
|
||
)}
|
||
/>
|
||
<span className="min-w-0">
|
||
<span className="block truncate font-semibold text-slate-800" title={item.title}>
|
||
{item.title}
|
||
</span>
|
||
{item.detail ? (
|
||
<span className="block line-clamp-2 whitespace-pre-wrap leading-5 text-slate-500">
|
||
{item.detail}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
<span
|
||
className={cn(
|
||
"rounded-full px-2 py-0.5 font-semibold",
|
||
progressStatusMeta[item.status].className
|
||
)}
|
||
>
|
||
{progressStatusMeta[item.status].label}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</motion.ol>
|
||
) : null}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TodoCard({ todoUpdate }: { todoUpdate: AgentTodoUpdate }) {
|
||
return (
|
||
<AgentNestedBlock icon={ListChecks} iconClassName="text-emerald-600" title="计划">
|
||
<motion.ol className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
|
||
<AnimatePresence initial={false} mode="popLayout">
|
||
{todoUpdate.todos.slice(0, 6).map((todo) => (
|
||
<motion.li
|
||
key={todo.id}
|
||
layout
|
||
className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs"
|
||
initial="initial"
|
||
animate="animate"
|
||
exit="exit"
|
||
variants={agentPanelItemVariants}
|
||
transition={agentLayoutTransition}
|
||
>
|
||
<span
|
||
className={cn(
|
||
"mt-0.5 grid h-3.5 w-3.5 place-items-center rounded-full border",
|
||
todoStatusMeta[todo.status].dotClassName
|
||
)}
|
||
>
|
||
{todo.status === "completed" ? <CheckCircle2 size={10} className="text-white" aria-hidden="true" /> : null}
|
||
</span>
|
||
<span className="min-w-0 truncate text-slate-700" title={todo.content}>
|
||
{todo.content}
|
||
</span>
|
||
<span className={cn("rounded-full px-2 py-0.5 font-semibold", todoStatusMeta[todo.status].className)}>
|
||
{todoStatusMeta[todo.status].label}
|
||
</span>
|
||
</motion.li>
|
||
))}
|
||
</AnimatePresence>
|
||
</motion.ol>
|
||
</AgentNestedBlock>
|
||
);
|
||
}
|
||
|
||
function PermissionList({
|
||
permissions,
|
||
onReplyPermission
|
||
}: {
|
||
permissions: AgentPermissionRequest[];
|
||
onReplyPermission?: (permission: AgentPermissionRequest, reply: AgentPermissionReply) => Promise<void> | void;
|
||
}) {
|
||
return (
|
||
<AgentNestedBlock icon={LockKeyhole} iconClassName="text-orange-600" title="权限请求">
|
||
<motion.div className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
|
||
<AnimatePresence initial={false} mode="popLayout">
|
||
{permissions.slice(-4).map((permission) => (
|
||
<AnimatedDetailItem key={permission.requestId}>
|
||
<PermissionRequestCard
|
||
permission={permission}
|
||
onReplyPermission={onReplyPermission}
|
||
/>
|
||
</AnimatedDetailItem>
|
||
))}
|
||
</AnimatePresence>
|
||
</motion.div>
|
||
</AgentNestedBlock>
|
||
);
|
||
}
|
||
|
||
function PermissionRequestCard({
|
||
permission,
|
||
onReplyPermission
|
||
}: {
|
||
permission: AgentPermissionRequest;
|
||
onReplyPermission?: (permission: AgentPermissionRequest, reply: AgentPermissionReply) => Promise<void> | void;
|
||
}) {
|
||
const actionable = permission.status === "pending" || permission.status === "error";
|
||
const submitting = permission.status === "submitting";
|
||
const canApproveAlways = permission.always.length > 0;
|
||
const disabled = !onReplyPermission || submitting;
|
||
const target = permission.target ?? (permission.patterns.join(" ") || permission.permission);
|
||
|
||
const reply = (nextReply: AgentPermissionReply) => {
|
||
if (!onReplyPermission || submitting) {
|
||
return;
|
||
}
|
||
void Promise.resolve(onReplyPermission(permission, nextReply));
|
||
};
|
||
|
||
return (
|
||
<div className="surface-reading rounded-lg px-2.5 py-2 text-xs">
|
||
<div className="grid grid-cols-[1fr_auto] gap-2">
|
||
<div className="min-w-0">
|
||
<p className="truncate font-semibold text-slate-800" title={permission.target ?? permission.permission}>
|
||
{getPermissionTitle(permission)}
|
||
</p>
|
||
<p className="mt-0.5 truncate font-mono text-xs text-slate-500" title={target}>
|
||
{target}
|
||
</p>
|
||
{permission.error ? (
|
||
<p className="mt-1 line-clamp-2 text-xs leading-4 text-red-600">{permission.error}</p>
|
||
) : null}
|
||
</div>
|
||
<span className={cn("self-start rounded-full px-2 py-0.5 font-semibold", permissionStatusMeta[permission.status].className)}>
|
||
{permissionStatusMeta[permission.status].label}
|
||
</span>
|
||
</div>
|
||
<AnimatePresence initial={false}>
|
||
{actionable ? (
|
||
<motion.div
|
||
key="permission-actions"
|
||
className="mt-2 flex flex-wrap items-center gap-1.5"
|
||
initial="initial"
|
||
animate="animate"
|
||
exit="exit"
|
||
variants={agentPanelSectionVariants}
|
||
style={{ overflow: "hidden" }}
|
||
>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
className="agent-panel-primary-action h-7 rounded-md px-2 text-xs"
|
||
disabled={disabled}
|
||
onClick={() => reply("once")}
|
||
>
|
||
<CheckCircle2 size={13} aria-hidden="true" />
|
||
允许一次
|
||
</Button>
|
||
{canApproveAlways ? (
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
className="h-7 rounded-md border-blue-200 bg-blue-50 px-2 text-xs text-blue-700 hover:bg-blue-100"
|
||
disabled={disabled}
|
||
onClick={() => reply("always")}
|
||
>
|
||
<ShieldCheck size={13} aria-hidden="true" />
|
||
始终允许
|
||
</Button>
|
||
) : null}
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
className="h-7 rounded-md border-red-200 bg-red-50 px-2 text-xs text-red-700 hover:bg-red-100"
|
||
disabled={disabled}
|
||
onClick={() => reply("reject")}
|
||
>
|
||
<XCircle size={13} aria-hidden="true" />
|
||
拒绝
|
||
</Button>
|
||
</motion.div>
|
||
) : null}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function QuestionList({
|
||
questions,
|
||
onReplyQuestion,
|
||
onRejectQuestion
|
||
}: {
|
||
questions: AgentQuestionRequest[];
|
||
onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise<void> | void;
|
||
onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
|
||
}) {
|
||
return (
|
||
<AgentNestedBlock icon={HelpCircle} iconClassName="text-blue-600" title="补充信息">
|
||
<motion.div className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
|
||
<AnimatePresence initial={false} mode="popLayout">
|
||
{questions.slice(-3).map((request) => (
|
||
<AnimatedDetailItem key={request.requestId}>
|
||
<QuestionRequestCard
|
||
request={request}
|
||
onReplyQuestion={onReplyQuestion}
|
||
onRejectQuestion={onRejectQuestion}
|
||
/>
|
||
</AnimatedDetailItem>
|
||
))}
|
||
</AnimatePresence>
|
||
</motion.div>
|
||
</AgentNestedBlock>
|
||
);
|
||
}
|
||
|
||
function QuestionRequestCard({
|
||
request,
|
||
onReplyQuestion,
|
||
onRejectQuestion
|
||
}: {
|
||
request: AgentQuestionRequest;
|
||
onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise<void> | void;
|
||
onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
|
||
}) {
|
||
const [draftAnswers, setDraftAnswers] = useState(() => createInitialQuestionAnswers(request));
|
||
const actionable = request.status === "pending" || request.status === "error";
|
||
const submitting = request.status === "submitting";
|
||
const disabled = submitting || !onReplyQuestion;
|
||
const answers = normalizeDraftAnswers(draftAnswers);
|
||
const canSubmit = actionable && answers.length === request.questions.length && answers.every((answer) => answer.length > 0);
|
||
|
||
const submit = () => {
|
||
if (!onReplyQuestion || !canSubmit) {
|
||
return;
|
||
}
|
||
void Promise.resolve(onReplyQuestion(request, answers));
|
||
};
|
||
|
||
const reject = () => {
|
||
if (!onRejectQuestion || submitting) {
|
||
return;
|
||
}
|
||
void Promise.resolve(onRejectQuestion(request));
|
||
};
|
||
|
||
return (
|
||
<div className="surface-reading rounded-lg px-2.5 py-2 text-xs">
|
||
<div className="mb-2 flex items-center justify-between gap-2">
|
||
<span className="min-w-0 truncate font-semibold text-slate-800">
|
||
{request.questions[0]?.header || "问题"}
|
||
</span>
|
||
<span className={cn("rounded-full px-2 py-0.5 font-semibold", questionStatusMeta[request.status].className)}>
|
||
{questionStatusMeta[request.status].label}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{request.questions.map((question, questionIndex) => (
|
||
<QuestionInput
|
||
key={`${request.requestId}-${questionIndex}`}
|
||
question={question}
|
||
questionIndex={questionIndex}
|
||
value={draftAnswers[questionIndex] ?? { selected: [], custom: "" }}
|
||
disabled={!actionable || submitting}
|
||
onChange={(nextValue) =>
|
||
setDraftAnswers((current) => current.map((item, index) => (index === questionIndex ? nextValue : item)))
|
||
}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{request.error ? (
|
||
<p className="mt-2 line-clamp-2 text-xs leading-4 text-red-600">{request.error}</p>
|
||
) : null}
|
||
{request.answers?.length ? (
|
||
<p className="mt-2 truncate text-slate-500" title={request.answers.map((answer) => answer.join("、")).join(";")}>
|
||
已答:{request.answers.map((answer) => answer.join("、")).join(";")}
|
||
</p>
|
||
) : null}
|
||
|
||
<AnimatePresence initial={false}>
|
||
{actionable ? (
|
||
<motion.div
|
||
key="question-actions"
|
||
className="mt-2 flex flex-wrap items-center gap-1.5"
|
||
initial="initial"
|
||
animate="animate"
|
||
exit="exit"
|
||
variants={agentPanelSectionVariants}
|
||
style={{ overflow: "hidden" }}
|
||
>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
className="agent-panel-primary-action h-7 rounded-md px-2 text-xs"
|
||
disabled={disabled || !canSubmit}
|
||
onClick={submit}
|
||
>
|
||
<CheckCircle2 size={13} aria-hidden="true" />
|
||
提交回答
|
||
</Button>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
className="h-7 rounded-md border-slate-200 bg-white px-2 text-xs text-slate-600 hover:bg-slate-50"
|
||
disabled={submitting || !onRejectQuestion}
|
||
onClick={reject}
|
||
>
|
||
<XCircle size={13} aria-hidden="true" />
|
||
跳过
|
||
</Button>
|
||
</motion.div>
|
||
) : null}
|
||
</AnimatePresence>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
type QuestionDraftAnswer = {
|
||
selected: string[];
|
||
custom: string;
|
||
};
|
||
|
||
function QuestionInput({
|
||
question,
|
||
questionIndex,
|
||
value,
|
||
disabled,
|
||
onChange
|
||
}: {
|
||
question: AgentQuestionInfo;
|
||
questionIndex: number;
|
||
value: QuestionDraftAnswer;
|
||
disabled: boolean;
|
||
onChange: (value: QuestionDraftAnswer) => void;
|
||
}) {
|
||
const name = `agent-question-${questionIndex}-${question.question}`;
|
||
const showCustomInput = question.custom || question.options.length === 0;
|
||
|
||
const toggleOption = (label: string, checked: boolean) => {
|
||
if (question.multiple) {
|
||
onChange({
|
||
...value,
|
||
selected: checked ? [...value.selected, label] : value.selected.filter((item) => item !== label)
|
||
});
|
||
return;
|
||
}
|
||
onChange({
|
||
...value,
|
||
selected: checked ? [label] : []
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-1.5">
|
||
<p className="leading-5 text-slate-700">{question.question}</p>
|
||
{question.options.length ? (
|
||
<div className="space-y-1">
|
||
{question.options.map((option) => {
|
||
const checked = value.selected.includes(option.label);
|
||
return (
|
||
<label
|
||
key={option.label}
|
||
className="surface-reading flex cursor-pointer items-start gap-2 rounded-md border border-slate-200 px-2 py-1.5 text-slate-600"
|
||
>
|
||
<input
|
||
type={question.multiple ? "checkbox" : "radio"}
|
||
name={name}
|
||
className="mt-0.5 h-3.5 w-3.5 accent-blue-600"
|
||
checked={checked}
|
||
disabled={disabled}
|
||
onChange={(event) => toggleOption(option.label, event.currentTarget.checked)}
|
||
/>
|
||
<span className="min-w-0">
|
||
<span className="block font-semibold text-slate-700">{option.label}</span>
|
||
{option.description ? (
|
||
<span className="block leading-4 text-slate-500">{option.description}</span>
|
||
) : null}
|
||
</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
) : null}
|
||
{showCustomInput ? (
|
||
<textarea
|
||
className="surface-reading min-h-16 w-full resize-none rounded-md border border-slate-200 px-2 py-1.5 text-xs leading-5 text-slate-700 outline-none transition focus:border-blue-300 focus:ring-2 focus:ring-blue-100 disabled:opacity-60"
|
||
placeholder="输入回答"
|
||
value={value.custom}
|
||
disabled={disabled}
|
||
onChange={(event) => onChange({ ...value, custom: event.currentTarget.value })}
|
||
/>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function createInitialQuestionAnswers(request: AgentQuestionRequest): QuestionDraftAnswer[] {
|
||
return request.questions.map((question, index) => ({
|
||
selected: request.answers?.[index] ?? [],
|
||
custom: ""
|
||
}));
|
||
}
|
||
|
||
function normalizeDraftAnswers(draftAnswers: QuestionDraftAnswer[]) {
|
||
return draftAnswers.map((answer) => {
|
||
const custom = answer.custom.trim();
|
||
return custom ? [...answer.selected, custom] : answer.selected;
|
||
});
|
||
}
|
||
|
||
function getPermissionTitle(permission: AgentPermissionRequest) {
|
||
if (permission.permission === "bash") return "执行终端命令";
|
||
if (permission.permission === "edit") return "修改文件内容";
|
||
if (permission.permission === "external_directory") return "访问工作区外目录";
|
||
return permission.permission || "工具权限请求";
|
||
}
|