feat(ui): add traffic and tool detail drawers

This commit is contained in:
pphua committed 2026-08-24 19:30:35 +08:00
1 parent fd2ddbc9d7
commit af3149901e
6 files changed
+436 -271

No files matched your search

+4 -3
View File
@@ -23,6 +23,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Markdown } from "@/components/markdown";
import { api } from "@/lib/api";
import { cn } from "@/lib/utils";
import type { Agent, AgentDetail, AgentTrigger, MCPServer, PromptVar, PromptVersion, Settings, SkillItem, Tool } from "@/lib/types";
@@ -397,9 +398,9 @@ export function AgentEditor({ agentKey, onSaved }: { agentKey: string; onSaved?:
<DialogTitle>渲染预览</DialogTitle>
<DialogDescription>所有 {`{{.Var}}`} 已由后端用示例值替换。</DialogDescription>
</DialogHeader>
<pre className="bg-muted max-h-[60vh] overflow-auto whitespace-pre-wrap rounded-md p-3 font-mono text-xs">
{preview}
</pre>
<div className="max-h-[60vh] overflow-auto rounded-md border bg-muted/30 p-3">
<Markdown text={preview} />
</div>
</DialogContent>
</Dialog>
<Button size="sm" onClick={savePrompt}>
+67
View File
@@ -0,0 +1,67 @@
"use client";
import * as React from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { cn, copyText } from "@/lib/utils";
type CopyButtonProps = {
// 要复制的文本;为空则按钮禁用。
text: string | null | undefined;
// 复制成功后的 toast 文案,默认「已复制」。
successMessage?: string;
label?: React.ReactNode;
size?: React.ComponentProps<typeof Button>["size"];
variant?: React.ComponentProps<typeof Button>["variant"];
className?: string;
};
// CopyButton 统一的「复制到剪贴板」按钮:内置成功/失败反馈,并在 HTTP 非安全上下文
// 下自动降级(见 copyText)。
export function CopyButton({
text,
successMessage = "已复制",
label = "复制",
size = "sm",
variant = "outline",
className,
}: CopyButtonProps) {
const [copied, setCopied] = React.useState(false);
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(() => {
return () => {
if (timer.current) clearTimeout(timer.current);
};
}, []);
async function handleCopy() {
if (!text) return;
const ok = await copyText(text);
if (ok) {
setCopied(true);
toast.success(successMessage);
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1500);
} else {
toast.error("复制失败,请手动选择文本复制");
}
}
return (
<Button
type="button"
size={size}
variant={variant}
className={cn(className)}
disabled={!text}
onClick={handleCopy}
>
{copied ? <CheckIcon /> : <CopyIcon />}
{label}
</Button>
);
}
+5 -2
View File
@@ -18,6 +18,7 @@ import {
import { toast } from "sonner";
import { api } from "@/lib/api";
import { Markdown } from "@/components/markdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { Activity } from "@/lib/types";
@@ -208,7 +209,7 @@ function InterceptCard({
}, [pendingId]);
async function decide(decision: "allowed" | "denied") {
if (!pendingId || deciding) return;
if (step.inherited || !pendingId || deciding) return;
setDeciding(true);
try {
await api.interceptDecide(pendingId, decision);
@@ -246,7 +247,9 @@ function InterceptCard({
</div>
</div>
{decided ? (
{step.inherited ? (
<Badge variant="outline">历史记录 · 只读</Badge>
) : decided ? (
<span className={
"shrink-0 rounded px-2 py-0.5 text-[11px] font-medium " +
(decided === "allowed"