feat(web): W4-F rebuild agent conversation and drawer with agentui components
- Port agentui (MIT, ashish200729/agentui@b2b752b) Message/MessageBubble/MessageScroller, StreamingResponse, ToolResult/ToolApproval, TodoList, ApprovalCard, CodeBlock/FileDiff, Citations, PromptInput into components/agentui; drop motion/shiki/Next, switch colors, type scale, radii, durations and easing to semantic tokens, honour reduced motion - Conversation: streaming cursor, collapsible thinking, tool cards with kind/scope/risk/ idempotency badges and audit idempotency key, prominent server-rejection state for both whitelist rejections and unauthorized errors, compaction markers, budget meters with over-limit state, approval cards (admin session only), two-column final report (model said vs system verified with task/record links), follow-scroll with jump-to-bottom - Composer: Enter send / Shift+Enter newline, stop = abort (confirmed), append, execute/plan_only switch, "execute this plan" from finished plan_only runs - Session list sidebar with status filter and usage; reconnect/error banners with retry - Drawer: route-derived context (entity/site/task), minimize to a live mini dock - Fix stream refetch race when the run switches from loading to finished Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
29cd10b27e
commit
8352e5e881
26 files changed
+3390
-585
No files matched your search
@@ -0,0 +1,139 @@
|
||||
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/approval-card/index.tsx + types.ts
|
||||
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
|
||||
// 改造:状态集按本项目 Approval 对齐——上游的 submitting 保留,changes-requested/answered 与多步问答模式去掉
|
||||
// (依赖 motion 的 Checkbox/Radio,且本项目审批只有批准/拒绝);新增 revoked/expired/consumed 终态;
|
||||
// 状态色换 warning/success/destructive 并配文字;按钮用项目 <Button>(胶囊,主操作 pill-primary);
|
||||
// 新增 `disabledReason`:按钮不可用时说明原因(如"只有控制台管理员会话能批准")。
|
||||
import { Check, Clock, LoaderCircle, MessageSquareText, Undo2, X } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { Disclosure } from "./disclosure.tsx";
|
||||
|
||||
export type ApprovalCardStatus =
|
||||
| "pending"
|
||||
| "submitting"
|
||||
| "approved"
|
||||
| "rejected"
|
||||
| "revoked"
|
||||
| "expired"
|
||||
| "consumed";
|
||||
|
||||
const LABEL: Record<ApprovalCardStatus, string> = {
|
||||
pending: "待批准",
|
||||
submitting: "提交中",
|
||||
approved: "已批准",
|
||||
rejected: "已拒绝",
|
||||
revoked: "已撤销",
|
||||
expired: "已过期",
|
||||
consumed: "已执行",
|
||||
};
|
||||
|
||||
const BADGE: Record<ApprovalCardStatus, string> = {
|
||||
pending: "border-warning/30 bg-warning/10 text-warning",
|
||||
submitting: "border-border bg-foreground/[0.04] text-muted-foreground",
|
||||
approved: "border-success/30 bg-success/10 text-success",
|
||||
consumed: "border-success/30 bg-success/10 text-success",
|
||||
rejected: "border-destructive/30 bg-destructive/10 text-destructive",
|
||||
revoked: "border-border bg-foreground/[0.04] text-muted-foreground",
|
||||
expired: "border-border bg-foreground/[0.04] text-muted-foreground",
|
||||
};
|
||||
|
||||
function StatusIcon({ status }: { status: ApprovalCardStatus }) {
|
||||
if (status === "submitting") return <LoaderCircle className="size-4 animate-spin motion-reduce:animate-none" />;
|
||||
if (status === "pending") return <MessageSquareText className="size-4" />;
|
||||
if (status === "rejected") return <X className="size-4" />;
|
||||
if (status === "expired") return <Clock className="size-4" />;
|
||||
if (status === "revoked") return <Undo2 className="size-4" />;
|
||||
return <Check className="size-4" />;
|
||||
}
|
||||
|
||||
export interface ApprovalCardProps {
|
||||
title?: ReactNode;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
status?: ApprovalCardStatus;
|
||||
badges?: ReactNode;
|
||||
onApprove?: () => void;
|
||||
onReject?: () => void;
|
||||
approveLabel?: ReactNode;
|
||||
rejectLabel?: ReactNode;
|
||||
/** 按钮不可用的原因;给了就禁用按钮并显示 */
|
||||
disabledReason?: string;
|
||||
/** 终态时的结论文字 */
|
||||
result?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ApprovalCard({
|
||||
title = "需要批准",
|
||||
description,
|
||||
children,
|
||||
status = "pending",
|
||||
badges,
|
||||
onApprove,
|
||||
onReject,
|
||||
approveLabel = "批准",
|
||||
rejectLabel = "拒绝",
|
||||
disabledReason,
|
||||
result,
|
||||
className,
|
||||
}: ApprovalCardProps) {
|
||||
const busy = status === "submitting";
|
||||
const interactive = status === "pending" || busy;
|
||||
return (
|
||||
<section
|
||||
data-state={status}
|
||||
aria-busy={busy}
|
||||
aria-label={typeof title === "string" ? title : "审批"}
|
||||
className={cn(
|
||||
"w-full min-w-0 overflow-hidden rounded-xl border p-3 text-sm",
|
||||
status === "pending" ? "border-warning/40 bg-warning/[0.06]" : "border-border/70",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
"mt-0.5 grid size-5 shrink-0 place-items-center text-muted-foreground",
|
||||
status === "pending" && "text-warning",
|
||||
(status === "approved" || status === "consumed") && "text-success",
|
||||
status === "rejected" && "text-destructive",
|
||||
)}
|
||||
>
|
||||
<StatusIcon status={status} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-start gap-x-3 gap-y-1">
|
||||
<h3 className="min-w-0 flex-1 text-body leading-5 font-medium text-foreground">{title}</h3>
|
||||
<span className={cn("shrink-0 rounded-full border px-2 py-0.5 text-label font-medium", BADGE[status])}>
|
||||
{LABEL[status]}
|
||||
</span>
|
||||
</div>
|
||||
{badges ? <div className="mt-1.5 flex flex-wrap items-center gap-1">{badges}</div> : null}
|
||||
{description ? <p className="mt-1.5 text-caption leading-5 text-muted-foreground">{description}</p> : null}
|
||||
{children ? <div className="mt-2.5">{children}</div> : null}
|
||||
<Disclosure open={interactive}>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
<Button size="sm" disabled={busy || !!disabledReason || !onApprove} onClick={onApprove}>
|
||||
{approveLabel}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={busy || !!disabledReason || !onReject}
|
||||
onClick={onReject}
|
||||
className="hover:text-destructive"
|
||||
>
|
||||
{rejectLabel}
|
||||
</Button>
|
||||
{disabledReason ? <span className="text-meta text-muted-foreground">{disabledReason}</span> : null}
|
||||
</div>
|
||||
</Disclosure>
|
||||
{!interactive ? <p className="mt-1.5 text-caption text-muted-foreground">{result ?? LABEL[status]}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user