前端不再直连上游 SSE,改为 POST /api/sentiment-agent/stream 由后台代理转发,支持关闭 TLS 校验并回传完整错误信息。 Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import { BotIcon, MessageSquareIcon, Radio, SquarePenIcon } from "lucide-react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { SENTIMENT_AGENT_ID } from "@/core/sentiment-agent/config";
|
|
import { readSentimentAgentProfile } from "@/core/sentiment-agent/profile";
|
|
import { desensitize } from "@/lib/desensitize";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { SentimentAgentProfileDialog } from "./sentiment-agent-profile-dialog";
|
|
|
|
/**
|
|
* 「舆情分析」类型下的虚拟智能体卡片:只存在于前端(不落库 /api/agents),
|
|
* 对话经 Gateway 转发外部 AG-UI 网关。名称 / 简介可编辑(localStorage),
|
|
* 视觉与普通智能体卡片保持一致(网格 / 列表两种布局)。
|
|
*/
|
|
export function SentimentAgentCard({
|
|
layout = "grid",
|
|
className,
|
|
}: {
|
|
layout?: "grid" | "list";
|
|
className?: string;
|
|
}) {
|
|
const router = useRouter();
|
|
const [profile, setProfile] = useState(() => readSentimentAgentProfile());
|
|
const [editOpen, setEditOpen] = useState(false);
|
|
|
|
// 编辑弹窗也可能挂在对话页,保存后广播事件,这里即时刷新卡片。
|
|
useEffect(() => {
|
|
const onChange = () => setProfile(readSentimentAgentProfile());
|
|
window.addEventListener("sentiment-agent-profile-changed", onChange);
|
|
return () =>
|
|
window.removeEventListener("sentiment-agent-profile-changed", onChange);
|
|
}, []);
|
|
|
|
const handleChat = () => {
|
|
router.push(`/page/workspace/agents/${SENTIMENT_AGENT_ID}/chats/new`);
|
|
};
|
|
|
|
const editButton = (
|
|
<Button
|
|
size="icon"
|
|
variant="ghost"
|
|
className="h-8 w-8 shrink-0"
|
|
onClick={() => setEditOpen(true)}
|
|
title="编辑名称与简介"
|
|
aria-label="编辑名称与简介"
|
|
>
|
|
<SquarePenIcon className="h-3.5 w-3.5" />
|
|
</Button>
|
|
);
|
|
|
|
const chatButton = (
|
|
<Button
|
|
size="sm"
|
|
className={cn(layout === "list" ? "shrink-0" : "w-full")}
|
|
onClick={handleChat}
|
|
>
|
|
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
|
|
对话
|
|
</Button>
|
|
);
|
|
|
|
const badge = (
|
|
<Badge variant="secondary" className="gap-1 text-xs">
|
|
<Radio className="size-3" />
|
|
舆情分析
|
|
</Badge>
|
|
);
|
|
|
|
const dialog = (
|
|
<SentimentAgentProfileDialog
|
|
open={editOpen}
|
|
onOpenChange={setEditOpen}
|
|
/>
|
|
);
|
|
|
|
if (layout === "list") {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"group flex min-w-0 items-center gap-3 rounded-lg border p-2.5 transition-shadow hover:shadow-sm dark:border-[#213D5E] dark:bg-[#02244C]",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
|
<BotIcon className="h-5 w-5" />
|
|
</div>
|
|
<div className="min-w-0 flex-1 overflow-hidden">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<span
|
|
className="min-w-0 truncate text-sm font-medium"
|
|
title={desensitize(profile.name)}
|
|
>
|
|
{desensitize(profile.name)}
|
|
</span>
|
|
{badge}
|
|
</div>
|
|
<p className="text-muted-foreground mt-0.5 truncate text-xs">
|
|
{desensitize(profile.description)}
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
{editButton}
|
|
{chatButton}
|
|
</div>
|
|
{dialog}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card
|
|
className={cn(
|
|
"group flex min-w-0 flex-col transition-shadow hover:shadow-md",
|
|
className,
|
|
)}
|
|
>
|
|
<CardHeader className="min-w-0 pb-2">
|
|
<div className="flex min-w-0 items-start justify-between gap-2">
|
|
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
|
<div className="agent-avatar bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
|
|
<BotIcon className="h-5 w-5" />
|
|
</div>
|
|
<div className="min-w-0 flex-1 overflow-hidden">
|
|
<CardTitle
|
|
className="block w-full min-w-0 overflow-hidden text-base text-ellipsis whitespace-nowrap"
|
|
title={desensitize(profile.name)}
|
|
>
|
|
{desensitize(profile.name)}
|
|
</CardTitle>
|
|
<div className="mt-0.5">{badge}</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 items-center justify-end gap-1">
|
|
{editButton}
|
|
</div>
|
|
</div>
|
|
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
|
{desensitize(profile.description)}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardFooter className="mt-auto flex flex-col items-stretch gap-2 pt-2">
|
|
{chatButton}
|
|
</CardFooter>
|
|
{dialog}
|
|
</Card>
|
|
);
|
|
}
|