Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
208a02a912
29 files changed
+853
-66
No files matched your search
@@ -20,9 +20,10 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { PromptInputButton } from "@/components/ai-elements/prompt-input";
|
import { PromptInputButton } from "@/components/ai-elements/prompt-input";
|
||||||
import { AgentManageDialog } from "@/components/workspace/agents/agent-manage-dialog";
|
import { AgentManageDialog } from "@/components/workspace/agents/agent-manage-dialog";
|
||||||
import { useAgentSelector } from "@/core/agents";
|
import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
|
||||||
import type { Agent } from "@/core/agents";
|
import type { Agent } from "@/core/agents";
|
||||||
import { getStoredAuth } from "@/core/auth";
|
import { getStoredAuth } from "@/core/auth";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface AgentSelectorProps {
|
interface AgentSelectorProps {
|
||||||
@@ -51,12 +52,21 @@ export function AgentSelector({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [manageOpen, setManageOpen] = useState(false);
|
const [manageOpen, setManageOpen] = useState(false);
|
||||||
const { agents, isLoading } = useAgentSelector();
|
const { agents, isLoading } = useAgentSelector();
|
||||||
|
// 弹窗列表只展示管理弹窗里配置的 agent(agents);但「已选中 agent」的名称解析要更宽——
|
||||||
|
// 侧边栏「常用智能体」可以选中未配置进管理弹窗的 agent(如用户自己的智能体),
|
||||||
|
// 这些 agent 也要能在胶囊上显示名称,故额外用「我的 / 广场」列表兜底解析(缓存与侧边栏共享)。
|
||||||
|
const { agents: mineAgents } = useAgentsPaginated({ scope: "mine", page: 1, pageSize: 20 });
|
||||||
|
const { agents: squareAgents } = useAgentsPaginated({ scope: "square", page: 1, pageSize: 20 });
|
||||||
const isAdmin = useMemo(() => getStoredAuth()?.system_role === "admin", []);
|
const isAdmin = useMemo(() => getStoredAuth()?.system_role === "admin", []);
|
||||||
|
|
||||||
const selectedAgent = useMemo<Agent | null>(() => {
|
const selectedAgent = useMemo<Agent | null>(() => {
|
||||||
if (!selectedAgentId) return null;
|
if (!selectedAgentId) return null;
|
||||||
return agents.find((a) => a.id === selectedAgentId) ?? null;
|
return (
|
||||||
}, [agents, selectedAgentId]);
|
[...agents, ...mineAgents, ...squareAgents].find(
|
||||||
|
(a) => a.id === selectedAgentId,
|
||||||
|
) ?? null
|
||||||
|
);
|
||||||
|
}, [agents, mineAgents, squareAgents, selectedAgentId]);
|
||||||
|
|
||||||
const handleSelect = (agent: Agent) => {
|
const handleSelect = (agent: Agent) => {
|
||||||
onAgentChange(agent.id);
|
onAgentChange(agent.id);
|
||||||
@@ -68,7 +78,7 @@ export function AgentSelector({
|
|||||||
onAgentChange(null);
|
onAgentChange(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerLabel = selectedAgent?.name ?? "选择 Agent";
|
const triggerLabel = selectedAgent ? desensitize(selectedAgent.name) : "选择 Agent";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -125,7 +135,7 @@ export function AgentSelector({
|
|||||||
{agents.map((agent) => (
|
{agents.map((agent) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
value={`${agent.name} ${agent.description ?? ""}`}
|
value={`${desensitize(agent.name)} ${desensitize(agent.description ?? "")}`}
|
||||||
onSelect={() => handleSelect(agent)}
|
onSelect={() => handleSelect(agent)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full min-w-0 items-start gap-2 overflow-hidden",
|
"flex w-full min-w-0 items-start gap-2 overflow-hidden",
|
||||||
@@ -134,10 +144,10 @@ export function AgentSelector({
|
|||||||
>
|
>
|
||||||
<BotIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
<BotIcon className="text-muted-foreground mt-0.5 size-4 shrink-0" />
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<span className="truncate text-sm">{agent.name}</span>
|
<span className="truncate text-sm">{desensitize(agent.name)}</span>
|
||||||
{agent.description && (
|
{agent.description && (
|
||||||
<span className="line-clamp-2 whitespace-normal break-words text-[10px] leading-snug text-muted-foreground">
|
<span className="line-clamp-2 whitespace-normal break-words text-[10px] leading-snug text-muted-foreground">
|
||||||
{agent.description}
|
{desensitize(agent.description)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ export function ArtifactFileDetail({
|
|||||||
threadId,
|
threadId,
|
||||||
onClose,
|
onClose,
|
||||||
allowVirtualMarkdownEdit = false,
|
allowVirtualMarkdownEdit = false,
|
||||||
|
disableDesensitize = false,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
filepath: string;
|
filepath: string;
|
||||||
@@ -134,6 +135,12 @@ export function ArtifactFileDetail({
|
|||||||
* 默认 false —— 主聊天页等其它调用方行为不变。
|
* 默认 false —— 主聊天页等其它调用方行为不变。
|
||||||
*/
|
*/
|
||||||
allowVirtualMarkdownEdit?: boolean;
|
allowVirtualMarkdownEdit?: boolean;
|
||||||
|
/**
|
||||||
|
* 关闭对预览内容的脱敏。多智能体会商页的「消息列表沙箱」按需求**不脱敏**沙箱内的
|
||||||
|
* 产物内容(用户要拿原始文件),故 Step2SandboxLayout 传 true;主聊天等其它调用方
|
||||||
|
* 默认 false,沿用全局脱敏。
|
||||||
|
*/
|
||||||
|
disableDesensitize?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select } = useArtifacts();
|
const { artifacts, setOpen, select } = useArtifacts();
|
||||||
@@ -802,7 +809,7 @@ export function ArtifactFileDetail({
|
|||||||
language === "html" ||
|
language === "html" ||
|
||||||
language === "mermaid") && (
|
language === "mermaid") && (
|
||||||
<ArtifactFilePreview
|
<ArtifactFilePreview
|
||||||
content={desensitize(displayContent ?? "")}
|
content={disableDesensitize ? (displayContent ?? "") : desensitize(displayContent ?? "")}
|
||||||
language={language ?? "text"}
|
language={language ?? "text"}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
publicShareCode={publicShareCode}
|
publicShareCode={publicShareCode}
|
||||||
@@ -815,7 +822,7 @@ export function ArtifactFileDetail({
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="size-full resize-none rounded-none border-none bg-transparent"
|
className="size-full resize-none rounded-none border-none bg-transparent"
|
||||||
value={
|
value={
|
||||||
canEditArtifact
|
canEditArtifact || disableDesensitize
|
||||||
? (displayContent ?? "")
|
? (displayContent ?? "")
|
||||||
: desensitize(displayContent ?? "")
|
: desensitize(displayContent ?? "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,586 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用问答(全量版 / full layout)新建对话左侧栏 —— 按「样式修改.png」参考图重画。
|
||||||
|
*
|
||||||
|
* 这是**全量版专属**的独立组件:因为全量版是新功能、新需求,若继续往
|
||||||
|
* 共享的 WorkspaceNavChatList / FrequentAgentsList / RecentChatList 里塞会越来越难
|
||||||
|
* 维护,故单独成一个组件,只在 `/page/workspace/chats*` 的全量布局下挂载,互不影响。
|
||||||
|
*
|
||||||
|
* 三块结构(自上而下):
|
||||||
|
* 1. 「新对话」胶囊按钮 + 收起整列的圆形按钮;
|
||||||
|
* 2. 「常用智能体」—— 彩色图标卡(名称 + 两行简介,命中为选中态:主色描边 + 右侧主色条);
|
||||||
|
* 3. 「最近的对话」—— 计数徽标 + 折叠、历史搜索框、会话列表(悬浮「更多」菜单:
|
||||||
|
* 重命名/置顶/收藏/分享/导出/删除)、「更多最近对话」入口,底部品牌信息。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
Download,
|
||||||
|
FileJson,
|
||||||
|
FileText,
|
||||||
|
MessageSquare,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Pin,
|
||||||
|
PinOff,
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Share2,
|
||||||
|
Star,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useParams, usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { ShareConversationDialog } from "@/components/workspace/thread-share-dialogs";
|
||||||
|
import { useAgentsPaginated, type Agent } from "@/core/agents";
|
||||||
|
import { getAPIClient } from "@/core/api";
|
||||||
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import {
|
||||||
|
exportThreadAsJSON,
|
||||||
|
exportThreadAsMarkdown,
|
||||||
|
} from "@/core/threads/export";
|
||||||
|
import {
|
||||||
|
useDeleteThread,
|
||||||
|
useRenameThread,
|
||||||
|
useThreads,
|
||||||
|
useToggleThreadFavorite,
|
||||||
|
useToggleThreadPin,
|
||||||
|
} from "@/core/threads/hooks";
|
||||||
|
import type { AgentThread, AgentThreadState } from "@/core/threads/types";
|
||||||
|
import {
|
||||||
|
isThreadFavorite,
|
||||||
|
isThreadPinned,
|
||||||
|
pathOfThread,
|
||||||
|
sortThreadsPinnedFirst,
|
||||||
|
titleOfThread,
|
||||||
|
} from "@/core/threads/utils";
|
||||||
|
import { env } from "@/env";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
import { isIMEComposing } from "@/lib/ime";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { SidebarCollapseButton } from "./workspace-header";
|
||||||
|
|
||||||
|
const NEW_CHAT_PATH = "/page/workspace/chats/new";
|
||||||
|
/** 「更多」常用智能体跳转目标:智能体管理页。 */
|
||||||
|
const MORE_AGENTS_HREF = "/page/workspace/agents";
|
||||||
|
/** 「更多最近对话」跳转目标。 */
|
||||||
|
const MORE_CHATS_HREF = "/page/workspace/chats";
|
||||||
|
/** 常用智能体最多展示个数。 */
|
||||||
|
const MAX_FREQUENT = 4;
|
||||||
|
|
||||||
|
// 智能体图标统一用 Bot 机器人图标;底色按 id 稳定散列到一组主题色,复刻参考图的彩色方块。
|
||||||
|
const AGENT_COLORS = [
|
||||||
|
{ from: "#fb7185", to: "#ef4444" },
|
||||||
|
{ from: "#60a5fa", to: "#3b82f6" },
|
||||||
|
{ from: "#fbbf24", to: "#f59e0b" },
|
||||||
|
{ from: "#34d399", to: "#10b981" },
|
||||||
|
{ from: "#a78bfa", to: "#8b5cf6" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function agentColor(id: string) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < id.length; i++) {
|
||||||
|
hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
|
||||||
|
}
|
||||||
|
return AGENT_COLORS[hash % AGENT_COLORS.length]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 折叠区块标题(带 chevron 与可选计数徽标)。 */
|
||||||
|
function SectionLabel({
|
||||||
|
label,
|
||||||
|
count,
|
||||||
|
collapsed,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
count?: number;
|
||||||
|
collapsed: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onToggle}
|
||||||
|
className="flex w-full items-center justify-between px-3 py-1 text-[11px] font-semibold tracking-[0.04em] text-muted-foreground/80 transition-colors hover:text-muted-foreground select-none"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span>{label}</span>
|
||||||
|
{count != null && (
|
||||||
|
<span className="rounded-full bg-muted px-1.5 py-0.5 text-[10px] font-normal text-muted-foreground tabular-nums">
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 shrink-0 transition-transform duration-200",
|
||||||
|
collapsed && "-rotate-90",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GeneralQaSidebar() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const router = useRouter();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// ── 区块折叠状态(持久化,键独立于旧组件,避免互相影响)──
|
||||||
|
const [agentsCollapsed, setAgentsCollapsed] = useState(
|
||||||
|
() => localStorage.getItem("gqa-sidebar:agents-collapsed") === "true",
|
||||||
|
);
|
||||||
|
const [chatsCollapsed, setChatsCollapsed] = useState(
|
||||||
|
() => localStorage.getItem("gqa-sidebar:chats-collapsed") === "true",
|
||||||
|
);
|
||||||
|
const toggleAgents = () =>
|
||||||
|
setAgentsCollapsed((v) => {
|
||||||
|
localStorage.setItem("gqa-sidebar:agents-collapsed", String(!v));
|
||||||
|
return !v;
|
||||||
|
});
|
||||||
|
const toggleChats = () =>
|
||||||
|
setChatsCollapsed((v) => {
|
||||||
|
localStorage.setItem("gqa-sidebar:chats-collapsed", String(!v));
|
||||||
|
return !v;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 常用智能体取数:置顶 → 我的 → 广场,去重补足到 MAX_FREQUENT(沿用旧逻辑)──
|
||||||
|
const { agents: mine } = useAgentsPaginated({ scope: "mine", page: 1, pageSize: 20 });
|
||||||
|
const { agents: square } = useAgentsPaginated({ scope: "square", page: 1, pageSize: 20 });
|
||||||
|
const frequent = useMemo<Agent[]>(() => {
|
||||||
|
const out: Agent[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const add = (agent: Agent | undefined) => {
|
||||||
|
if (!agent || out.length >= MAX_FREQUENT || seen.has(agent.id)) return;
|
||||||
|
seen.add(agent.id);
|
||||||
|
out.push(agent);
|
||||||
|
};
|
||||||
|
[...mine, ...square].filter((a) => a.is_pinned).forEach(add);
|
||||||
|
mine.forEach(add);
|
||||||
|
square.forEach(add);
|
||||||
|
return out;
|
||||||
|
}, [mine, square]);
|
||||||
|
|
||||||
|
// 当前嵌入的智能体(URL 上的 ?agent=):用于高亮对应卡片。
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const activeAgentId = searchParams.get("agent");
|
||||||
|
|
||||||
|
// 点击常用智能体 = 在通用问答右侧**原地嵌入**该智能体的对话页:停留在 /chats/new,
|
||||||
|
// 仅给 URL 加 ?agent=<id>,由 NewChatContent 把右侧问答区换成 AgentChatPage。
|
||||||
|
// 不跳转独立页面、左侧栏不变。每次点击都重开一个干净会话(NewChatContent 的 key)。
|
||||||
|
const handleSelectAgent = useCallback(
|
||||||
|
(agentId: string) => {
|
||||||
|
navigate(`/page/workspace/chats/new?agent=${encodeURIComponent(agentId)}`);
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 最近的对话取数 + 搜索 ──
|
||||||
|
const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } = useParams<{
|
||||||
|
thread_id: string;
|
||||||
|
agent_id?: string;
|
||||||
|
}>();
|
||||||
|
const { data: threads = [] } = useThreads();
|
||||||
|
const { mutate: deleteThread } = useDeleteThread();
|
||||||
|
const { mutate: renameThread } = useRenameThread();
|
||||||
|
const { toggle: togglePin } = useToggleThreadPin();
|
||||||
|
const { toggle: toggleFavorite } = useToggleThreadFavorite();
|
||||||
|
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||||
|
const [renameThreadId, setRenameThreadId] = useState<string | null>(null);
|
||||||
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
const [shareThreadId, setShareThreadId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const visibleThreads = useMemo(
|
||||||
|
() =>
|
||||||
|
sortThreadsPinnedFirst(
|
||||||
|
threads.filter(
|
||||||
|
(thread) =>
|
||||||
|
thread.metadata?.thread_type !== "scheduler" &&
|
||||||
|
thread.metadata?.thread_type !== "bootstrap" &&
|
||||||
|
thread.metadata?.thread_type !== "roundtable",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[threads],
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredThreads = useMemo(() => {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
const list = q
|
||||||
|
? visibleThreads.filter((thread) =>
|
||||||
|
titleOfThread(thread, t.pages.newChat).toLowerCase().includes(q),
|
||||||
|
)
|
||||||
|
: visibleThreads;
|
||||||
|
// 未搜索时与旧版一致:最多展示 10 条
|
||||||
|
return q ? list : list.slice(0, 10);
|
||||||
|
}, [visibleThreads, query, t.pages.newChat]);
|
||||||
|
|
||||||
|
const handleNewChatClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return;
|
||||||
|
event.preventDefault();
|
||||||
|
navigate(NEW_CHAT_PATH, { replace: true, state: { resetAt: Date.now() } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(threadId: string) => {
|
||||||
|
deleteThread({ threadId });
|
||||||
|
if (threadId === threadIdFromPath) {
|
||||||
|
const threadIndex = threads.findIndex((th) => th.thread_id === threadId);
|
||||||
|
let nextThreadPath = pathOfThread("new", { agent_id: agentIdFromPath });
|
||||||
|
if (threadIndex > -1) {
|
||||||
|
if (threads[threadIndex + 1]) {
|
||||||
|
nextThreadPath = pathOfThread(threads[threadIndex + 1]!);
|
||||||
|
} else if (threads[threadIndex - 1]) {
|
||||||
|
nextThreadPath = pathOfThread(threads[threadIndex - 1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void router.push(nextThreadPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[agentIdFromPath, deleteThread, router, threadIdFromPath, threads],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRenameClick = useCallback((threadId: string, currentTitle: string) => {
|
||||||
|
setRenameThreadId(threadId);
|
||||||
|
setRenameValue(currentTitle);
|
||||||
|
setRenameDialogOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRenameSubmit = useCallback(() => {
|
||||||
|
if (renameThreadId && renameValue.trim()) {
|
||||||
|
renameThread({ threadId: renameThreadId, title: renameValue.trim() });
|
||||||
|
setRenameDialogOpen(false);
|
||||||
|
setRenameThreadId(null);
|
||||||
|
setRenameValue("");
|
||||||
|
}
|
||||||
|
}, [renameThread, renameThreadId, renameValue]);
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async (thread: AgentThread, format: "markdown" | "json") => {
|
||||||
|
try {
|
||||||
|
const apiClient = getAPIClient();
|
||||||
|
const state = await apiClient.threads.getState<AgentThreadState>(
|
||||||
|
thread.thread_id,
|
||||||
|
);
|
||||||
|
const messages = state.values?.messages ?? [];
|
||||||
|
if (messages.length === 0) {
|
||||||
|
toast.error(t.conversation.noMessages);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (format === "markdown") exportThreadAsMarkdown(thread, messages);
|
||||||
|
else exportThreadAsJSON(thread, messages);
|
||||||
|
toast.success(t.common.exportSuccess);
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to export conversation");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isStatic = env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full min-h-0 flex-col bg-white">
|
||||||
|
{/* ── 顶部:新对话胶囊 + 收起按钮 ── */}
|
||||||
|
<div className="flex items-center gap-2 px-3 pt-3 pb-2">
|
||||||
|
{/* 新对话按钮:完全自定义样式(不复用 Button variant)。borderColor / color 走
|
||||||
|
inline style,避免被全局 border-color(--border)覆盖;无阴影。浅主色底 + 主色描边
|
||||||
|
+ 5px 圆角,全部走 --primary token,自动适配红/蓝/深蓝主题。 */}
|
||||||
|
<Link
|
||||||
|
href={NEW_CHAT_PATH}
|
||||||
|
onClick={handleNewChatClick}
|
||||||
|
style={{ borderColor: "var(--primary)", color: "var(--primary)" }}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 min-w-0 flex-1 items-center justify-center gap-1.5 rounded-[5px] text-[14px] font-semibold transition-colors",
|
||||||
|
"border border-solid",
|
||||||
|
"bg-[color-mix(in_srgb,var(--primary)_12%,white)] hover:bg-[color-mix(in_srgb,var(--primary)_20%,white)]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="size-4 stroke-[2.5]" />
|
||||||
|
<span>{t.sidebar.newChat}</span>
|
||||||
|
</Link>
|
||||||
|
<SidebarCollapseButton className="size-9 shrink-0 rounded-full border border-border bg-card text-muted-foreground shadow-sm hover:bg-accent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-2">
|
||||||
|
{/* ── 常用智能体 ── */}
|
||||||
|
{frequent.length > 0 && (
|
||||||
|
<div className="pt-2">
|
||||||
|
<SectionLabel
|
||||||
|
label="常用智能体"
|
||||||
|
collapsed={agentsCollapsed}
|
||||||
|
onToggle={toggleAgents}
|
||||||
|
/>
|
||||||
|
{!agentsCollapsed && (
|
||||||
|
<div className="mt-1 flex flex-col gap-1">
|
||||||
|
{frequent.map((agent) => {
|
||||||
|
const isActive = activeAgentId === agent.id;
|
||||||
|
const { from, to } = agentColor(agent.id);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={agent.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectAgent(agent.id)}
|
||||||
|
className={cn(
|
||||||
|
"group relative flex w-full items-start gap-2.5 rounded-xl border px-2.5 py-2 text-left transition-colors",
|
||||||
|
isActive
|
||||||
|
? "border-primary/40 bg-primary/5"
|
||||||
|
: "border-transparent hover:border-border hover:bg-accent/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="mt-0.5 flex size-9 shrink-0 items-center justify-center rounded-lg text-white shadow-sm"
|
||||||
|
style={{ backgroundImage: `linear-gradient(135deg, ${from}, ${to})` }}
|
||||||
|
>
|
||||||
|
<Bot className="size-[18px]" />
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"block truncate text-[13px] font-semibold",
|
||||||
|
isActive ? "text-primary" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{desensitize(agent.name)}
|
||||||
|
</span>
|
||||||
|
<span className="mt-0.5 block truncate text-[11px] leading-snug text-muted-foreground">
|
||||||
|
{agent.description ? desensitize(agent.description) : "智能体助手"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="absolute top-1/2 right-1 h-6 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<Link
|
||||||
|
href={MORE_AGENTS_HREF}
|
||||||
|
className="flex items-center justify-between rounded-lg px-3 py-1.5 text-[12px] text-muted-foreground/70 transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<span>{t.common.more}</span>
|
||||||
|
<ChevronRight className="size-3.5 shrink-0" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── 最近的对话 ── */}
|
||||||
|
<div className="mt-3 border-t border-sidebar-border/60 pt-3">
|
||||||
|
<SectionLabel
|
||||||
|
label={t.sidebar.recentChats}
|
||||||
|
count={visibleThreads.length}
|
||||||
|
collapsed={chatsCollapsed}
|
||||||
|
onToggle={toggleChats}
|
||||||
|
/>
|
||||||
|
{!chatsCollapsed && (
|
||||||
|
<div className="mt-1.5">
|
||||||
|
{/* 历史会话搜索框 */}
|
||||||
|
<div className="relative mb-1.5 px-1">
|
||||||
|
<Search className="pointer-events-none absolute top-1/2 left-3.5 size-3.5 -translate-y-1/2 text-muted-foreground/60" />
|
||||||
|
<Input
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="搜索历史会话…"
|
||||||
|
className="h-8 rounded-lg border-border bg-card pl-8 text-[12px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
{filteredThreads.length === 0 ? (
|
||||||
|
<div className="px-3 py-4 text-center text-[12px] text-muted-foreground/70">
|
||||||
|
{query ? "没有匹配的会话" : t.chats.noChats}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredThreads.map((thread) => {
|
||||||
|
const isActive = pathOfThread(thread) === pathname;
|
||||||
|
const pinned = isThreadPinned(thread);
|
||||||
|
const favorited = isThreadFavorite(thread);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={thread.thread_id}
|
||||||
|
className="group/item relative flex items-center"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={pathOfThread(thread)}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg px-2.5 text-[13px] transition-colors",
|
||||||
|
isActive
|
||||||
|
? "border border-primary/40 bg-primary/5 font-medium text-primary"
|
||||||
|
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<MessageSquare
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 shrink-0",
|
||||||
|
isActive ? "text-primary" : "text-muted-foreground/70",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{pinned && (
|
||||||
|
<Pin className="size-3 shrink-0 text-muted-foreground/70" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">
|
||||||
|
{desensitize(titleOfThread(thread, t.pages.newChat))}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<span className="absolute top-1/2 right-1 h-5 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
{!isStatic && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-1 flex size-6 items-center justify-center rounded-md text-muted-foreground opacity-0 transition-opacity hover:bg-background group-hover/item:opacity-100 data-[state=open]:opacity-100"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">{t.common.more}</span>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-48 rounded-lg" side="right" align="start">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() =>
|
||||||
|
handleRenameClick(
|
||||||
|
thread.thread_id,
|
||||||
|
titleOfThread(thread, t.pages.newChat),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Pencil className="text-muted-foreground" />
|
||||||
|
<span>{t.common.rename}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => togglePin(thread.thread_id, !pinned)}>
|
||||||
|
{pinned ? (
|
||||||
|
<PinOff className="text-muted-foreground" />
|
||||||
|
) : (
|
||||||
|
<Pin className="text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<span>{pinned ? t.common.unpin : t.common.pin}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => toggleFavorite(thread.thread_id, !favorited)}
|
||||||
|
>
|
||||||
|
<Star
|
||||||
|
className={
|
||||||
|
favorited
|
||||||
|
? "fill-amber-400 text-amber-400"
|
||||||
|
: "text-muted-foreground"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>{favorited ? t.common.unfavorite : t.common.favorite}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => setShareThreadId(thread.thread_id)}>
|
||||||
|
<Share2 className="text-muted-foreground" />
|
||||||
|
<span>{t.common.share}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<Download className="text-muted-foreground" />
|
||||||
|
<span>{t.common.export}</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem onSelect={() => handleExport(thread, "markdown")}>
|
||||||
|
<FileText className="text-muted-foreground" />
|
||||||
|
<span>{t.common.exportAsMarkdown}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => handleExport(thread, "json")}>
|
||||||
|
<FileJson className="text-muted-foreground" />
|
||||||
|
<span>{t.common.exportAsJSON}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onSelect={() => handleDelete(thread.thread_id)}>
|
||||||
|
<Trash2 className="text-muted-foreground" />
|
||||||
|
<span>{t.common.delete}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
{!query && (
|
||||||
|
<Link
|
||||||
|
href={MORE_CHATS_HREF}
|
||||||
|
className="mt-0.5 flex items-center justify-between rounded-lg px-3 py-1.5 text-[12px] text-muted-foreground/70 transition-colors hover:bg-accent/50 hover:text-foreground"
|
||||||
|
>
|
||||||
|
<span>更多最近对话</span>
|
||||||
|
<ChevronRight className="size-3.5 shrink-0" />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 重命名弹窗 */}
|
||||||
|
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t.common.rename}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<Input
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
placeholder={t.common.rename}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !isIMEComposing(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRenameSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setRenameDialogOpen(false)}>
|
||||||
|
{t.common.cancel}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRenameSubmit}>{t.common.save}</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 分享弹窗 */}
|
||||||
|
<ShareConversationDialog
|
||||||
|
threadId={shareThreadId}
|
||||||
|
open={shareThreadId !== null}
|
||||||
|
onOpenChange={(o) => {
|
||||||
|
if (!o) setShareThreadId(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import { useTheme } from "@/contexts/ThemeContext";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { FrequentAgentsList } from "./frequent-agents-list";
|
import { GeneralQaSidebar } from "./general-qa-sidebar";
|
||||||
import { RecentChatList } from "./recent-chat-list";
|
import { RecentChatList } from "./recent-chat-list";
|
||||||
import { RecentRoundtableList } from "./recent-roundtable-list";
|
import { RecentRoundtableList } from "./recent-roundtable-list";
|
||||||
import { RecentWritingList } from "./recent-writing-list";
|
import { RecentWritingList } from "./recent-writing-list";
|
||||||
@@ -30,7 +30,8 @@ export function WorkspaceSidebar({
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const useWhiteSidebar = compactChrome && theme === "light";
|
const useWhiteSidebar = compactChrome && theme === "light";
|
||||||
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
// 通用问答(全量模式)左栏:新建回答 → 常用智能体 → 聊天记录(需求 0616 #1)。
|
||||||
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;其余 workspace 页保持原样。
|
// 仅在通用问答(/page/workspace/chats*)下启用三块式布局;点击常用智能体后是
|
||||||
|
// 「原地嵌入」(停留在 /chats/new?agent=...,仅右侧问答区换成智能体对话),左侧栏不变。
|
||||||
const isGeneralQa = pathname.startsWith("/page/workspace/chats");
|
const isGeneralQa = pathname.startsWith("/page/workspace/chats");
|
||||||
|
|
||||||
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
// 紧凑模式:用「外层定宽容器 + transition-[width]」做展开/收起过渡动画。
|
||||||
@@ -58,13 +59,16 @@ export function WorkspaceSidebar({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<WorkspaceNavChatList showCollapseTrigger compact />
|
{isGeneralQa ? (
|
||||||
{isGeneralQa && <FrequentAgentsList />}
|
// 全量版通用问答(/page/workspace/chats*):新建对话侧栏独立成
|
||||||
<RecentChatList
|
// GeneralQaSidebar(按 0616 样式参考图重画),与其余 workspace 页解耦。
|
||||||
compact
|
<GeneralQaSidebar />
|
||||||
limit={isGeneralQa ? 10 : undefined}
|
) : (
|
||||||
moreHref={isGeneralQa ? "/page/workspace/chats" : undefined}
|
<>
|
||||||
/>
|
<WorkspaceNavChatList showCollapseTrigger compact />
|
||||||
|
<RecentChatList compact />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarRail />
|
<SidebarRail />
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
* value: 脱敏后替换词
|
* value: 脱敏后替换词
|
||||||
*/
|
*/
|
||||||
export const DESENSITIZE_MAP: Record<string, string> = {
|
export const DESENSITIZE_MAP: Record<string, string> = {
|
||||||
|
方案: "FA",
|
||||||
|
设计: "SJ",
|
||||||
|
任务: "RW",
|
||||||
态势: "TS",
|
态势: "TS",
|
||||||
滨海作战体系: "第一张网",
|
滨海作战体系: "第一张网",
|
||||||
滨海作战部队: "第一张网",
|
滨海作战部队: "第一张网",
|
||||||
@@ -72,6 +75,26 @@ export function desensitize(text: string): string {
|
|||||||
return text.replace(_regex, (matched) => DESENSITIZE_MAP[matched] ?? matched);
|
return text.replace(_regex, (matched) => DESENSITIZE_MAP[matched] ?? matched);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深度脱敏:递归遍历对象 / 数组,对其中所有字符串值做 {@link desensitize},
|
||||||
|
* 其余类型原样保留。返回**新值**(不修改入参),用于对结构化数据(如大屏 report-json
|
||||||
|
* 解析出的对象)整体脱敏后再渲染。
|
||||||
|
*
|
||||||
|
* 注意:仅用于「展示」路径——不要用它改写要回传后端 / 落库的数据。
|
||||||
|
*/
|
||||||
|
export function desensitizeDeep<T>(value: T): T {
|
||||||
|
if (typeof value === "string") return desensitize(value) as unknown as T;
|
||||||
|
if (Array.isArray(value)) return value.map((v) => desensitizeDeep(v)) as unknown as T;
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
out[k] = desensitizeDeep(v);
|
||||||
|
}
|
||||||
|
return out as T;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 运行时动态追加脱敏词,追加后自动重编译正则。
|
* 运行时动态追加脱敏词,追加后自动重编译正则。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -31,15 +31,23 @@ import { textOfMessage } from "@/core/threads/utils";
|
|||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { desensitize } from "@/lib/desensitize";
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
|
||||||
export default function AgentChatPage() {
|
export default function AgentChatPage({
|
||||||
|
embeddedAgentId,
|
||||||
|
}: {
|
||||||
|
/** 当作为「通用问答右侧嵌入」渲染时由父组件传入的 agent id(替代路由参数)。
|
||||||
|
* 传入即视为嵌入态:路由仍停留在 /chats/new,仅右侧问答区换成该智能体的对话。 */
|
||||||
|
embeddedAgentId?: string;
|
||||||
|
} = {}) {
|
||||||
const routeBase = useRouteBase();
|
const routeBase = useRouteBase();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [showFollowups, setShowFollowups] = useState(false);
|
const [showFollowups, setShowFollowups] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { agent_id } = useParams<{
|
const { agent_id: agentIdFromPath } = useParams<{
|
||||||
agent_id: string;
|
agent_id: string;
|
||||||
}>();
|
}>();
|
||||||
|
const agent_id = embeddedAgentId ?? agentIdFromPath;
|
||||||
|
const isEmbedded = !!embeddedAgentId;
|
||||||
|
|
||||||
// If ChatPage stashed an opening message for this agent, skip the landing
|
// If ChatPage stashed an opening message for this agent, skip the landing
|
||||||
// screen on first render so the user doesn't see a flash of the welcome UI
|
// screen on first render so the user doesn't see a flash of the welcome UI
|
||||||
@@ -208,7 +216,13 @@ export default function AgentChatPage() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`${routeBase}/agents/${agent_id}/chats/new`);
|
// 嵌入态:保持停留在通用问答右侧,开同一智能体的新对话;
|
||||||
|
// 非嵌入态:走该智能体的独立新对话页(按当前路由 base)。
|
||||||
|
router.push(
|
||||||
|
isEmbedded
|
||||||
|
? `/page/workspace/chats/new?agent=${agent_id}`
|
||||||
|
: `${routeBase}/agents/${agent_id}/chats/new`,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusSquare /> {t.agents.newChat}
|
<PlusSquare /> {t.agents.newChat}
|
||||||
|
|||||||
@@ -31,10 +31,13 @@ import {
|
|||||||
type BusinessMapping,
|
type BusinessMapping,
|
||||||
} from "@/strategy-components/api/business-mapping";
|
} from "@/strategy-components/api/business-mapping";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
|
|
||||||
export default function BusinessChainMappingPage() {
|
export default function BusinessChainMappingPage() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
// 整片脱敏:业务链条映射页展示文本(链条名/描述/席位名/标题)。输入框 Hook 自动跳过。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
|
|
||||||
const [rows, setRows] = useState<BusinessMapping[]>([]);
|
const [rows, setRows] = useState<BusinessMapping[]>([]);
|
||||||
const [agents, setAgents] = useState<Agent[]>([]);
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
@@ -160,7 +163,7 @@ export default function BusinessChainMappingPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
|
<div ref={dzRef} className={cn(shellClass, "[&_.t-input]:rounded-[5px] [&_.t-input__wrap]:rounded-[5px]")}>
|
||||||
<div className="mx-auto flex min-h-full max-w-7xl flex-col gap-6 px-6 py-7 lg:px-8">
|
<div className="mx-auto flex min-h-full max-w-7xl flex-col gap-6 px-6 py-7 lg:px-8">
|
||||||
|
|
||||||
{/* ── 页头 banner ── */}
|
{/* ── 页头 banner ── */}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout
|
|||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PanelLeftOpen } from "lucide-react";
|
import { PanelLeftOpen } from "lucide-react";
|
||||||
import { useAgentSelector } from "@/core/agents";
|
import { useAgentSelector, useAgentsPaginated } from "@/core/agents";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
@@ -130,6 +130,10 @@ export default function ChatPage() {
|
|||||||
const browserContextAutoSubmitRef = useRef<string | null>(null);
|
const browserContextAutoSubmitRef = useRef<string | null>(null);
|
||||||
const { agents: selectorAgents, isLoading: selectorLoading } =
|
const { agents: selectorAgents, isLoading: selectorLoading } =
|
||||||
useAgentSelector();
|
useAgentSelector();
|
||||||
|
// 已选 agent 的存在性校验需要更宽的来源:侧边栏可选未配置进管理弹窗的 agent
|
||||||
|
// (用户自己的智能体),这些也算有效,故把「我的 / 广场」一并纳入校验集(缓存共享)。
|
||||||
|
const { agents: mineAgents } = useAgentsPaginated({ scope: "mine", page: 1, pageSize: 20 });
|
||||||
|
const { agents: squareAgents } = useAgentsPaginated({ scope: "square", page: 1, pageSize: 20 });
|
||||||
const { selectedArtifact } = useArtifacts();
|
const { selectedArtifact } = useArtifacts();
|
||||||
useSpecificChatMode();
|
useSpecificChatMode();
|
||||||
|
|
||||||
@@ -142,7 +146,7 @@ export default function ChatPage() {
|
|||||||
setHasStartedConversation(false);
|
setHasStartedConversation(false);
|
||||||
setPromptPrefixOn(false);
|
setPromptPrefixOn(false);
|
||||||
// Pre-select the agent the user last started a conversation with so a
|
// Pre-select the agent the user last started a conversation with so a
|
||||||
// fresh chat defaults to it. Validated against the selector list below.
|
// fresh chat defaults to it. Validated against the known agent list below.
|
||||||
setSelectedAgentId(getLastAgentId());
|
setSelectedAgentId(getLastAgentId());
|
||||||
}
|
}
|
||||||
}, [isNewThread, threadId]);
|
}, [isNewThread, threadId]);
|
||||||
@@ -152,10 +156,14 @@ export default function ChatPage() {
|
|||||||
// dead agent. Wait for the list to load before deciding.
|
// dead agent. Wait for the list to load before deciding.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectorLoading || !selectedAgentId) return;
|
if (selectorLoading || !selectedAgentId) return;
|
||||||
if (!selectorAgents.some((a) => a.id === selectedAgentId)) {
|
// 校验集 = 管理弹窗 ∪ 我的 ∪ 广场;任一存在即视为有效(含用户未配置进管理弹窗
|
||||||
|
// 的自有智能体)。三者都还没加载出来时不做清除,避免把恢复值误删。
|
||||||
|
const known = [...selectorAgents, ...mineAgents, ...squareAgents];
|
||||||
|
if (known.length === 0) return;
|
||||||
|
if (!known.some((a) => a.id === selectedAgentId)) {
|
||||||
setSelectedAgentId(null);
|
setSelectedAgentId(null);
|
||||||
}
|
}
|
||||||
}, [selectedAgentId, selectorAgents, selectorLoading]);
|
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,21 @@ export function ChatRuntime({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用问答新对话内容区:默认渲染 ChatPage;当 URL 带 ``?agent=<id>``(点击左栏
|
||||||
|
* 「常用智能体」时由侧边栏写入)时,**在右侧问答区原地嵌入**该智能体的对话页
|
||||||
|
* (AgentChatPage),路由仍停留在 /chats/new,不跳转出去、不更换左侧栏。
|
||||||
|
* key 绑定 location.key —— 每次点击(即便同一 agent)都重开一个干净的会话。
|
||||||
|
*/
|
||||||
|
function NewChatContent() {
|
||||||
|
const location = useLocation();
|
||||||
|
const agentId = new URLSearchParams(location.search).get('agent');
|
||||||
|
if (agentId) {
|
||||||
|
return <AgentChatPage key={`${agentId}:${location.key}`} embeddedAgentId={agentId} />;
|
||||||
|
}
|
||||||
|
return <ChatPage />;
|
||||||
|
}
|
||||||
|
|
||||||
function NewChatEntryRoute({
|
function NewChatEntryRoute({
|
||||||
layoutProps,
|
layoutProps,
|
||||||
}: {
|
}: {
|
||||||
@@ -163,7 +178,7 @@ function NewChatEntryRoute({
|
|||||||
return (
|
return (
|
||||||
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
|
<WorkspaceLayout {...layoutProps} defaultHistorySidebarOpen>
|
||||||
<ChatRuntime>
|
<ChatRuntime>
|
||||||
<ChatPage />
|
<NewChatContent />
|
||||||
</ChatRuntime>
|
</ChatRuntime>
|
||||||
</WorkspaceLayout>
|
</WorkspaceLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
|
|
||||||
import { listAgents } from "@/core/agents";
|
import { listAgents } from "@/core/agents";
|
||||||
|
|
||||||
@@ -212,9 +213,11 @@ export function BusinessChainListDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onConfigure,
|
onConfigure,
|
||||||
}: BusinessChainListDialogProps) {
|
}: BusinessChainListDialogProps) {
|
||||||
|
// 整片脱敏:弹窗内链条列表(链条名/描述/席位名/标题)。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
|
<DialogContent ref={dzRef} className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
|
||||||
<DialogHeader className="shrink-0">
|
<DialogHeader className="shrink-0">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<LayersIcon className="size-5 text-primary" />
|
<LayersIcon className="size-5 text-primary" />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { DialogFooter } from "@/components/ui/dialog";
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
import { listAgents } from "@/core/agents";
|
import { listAgents } from "@/core/agents";
|
||||||
|
|
||||||
import { filterRecommendCandidates } from "../api/recommend";
|
import { filterRecommendCandidates } from "../api/recommend";
|
||||||
@@ -161,9 +162,12 @@ export function BusinessChainPicker({
|
|||||||
onConfirm({ agents, mode: "chain", chain: chainMeta });
|
onConfirm({ agents, mode: "chain", chain: chainMeta });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 整片脱敏:链条卡片区(链条名/描述/席位智能体名)。输入框不受影响(Hook 跳过)。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<section ref={dzRef} className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
{/* 筛选:我的 / 公共 */}
|
{/* 筛选:我的 / 公共 */}
|
||||||
<div className="mb-2 flex shrink-0 items-center gap-1 self-start rounded-lg border border-border bg-muted/40 p-0.5">
|
<div className="mb-2 flex shrink-0 items-center gap-1 self-start rounded-lg border border-border bg-muted/40 p-0.5">
|
||||||
{(["mine", "public"] as const).map((s) => (
|
{(["mine", "public"] as const).map((s) => (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
import { CheckIcon, ChevronRightIcon, Loader2Icon } from "lucide-react";
|
import { CheckIcon, ChevronRightIcon, Loader2Icon } from "lucide-react";
|
||||||
|
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { getAvatar } from "./Avatars";
|
import { getAvatar } from "./Avatars";
|
||||||
@@ -137,7 +138,7 @@ export function DagFlowChart({
|
|||||||
return (
|
return (
|
||||||
<FlowNode
|
<FlowNode
|
||||||
key={agentId}
|
key={agentId}
|
||||||
label={meta.name}
|
label={desensitize(meta.name)}
|
||||||
avatarType={meta.avatarType}
|
avatarType={meta.avatarType}
|
||||||
state={state}
|
state={state}
|
||||||
compact={compact}
|
compact={compact}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { CheckIcon, Loader2Icon, Network, X } from "lucide-react";
|
import { CheckIcon, Loader2Icon, Network, X } from "lucide-react";
|
||||||
|
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { getAvatar } from "./Avatars";
|
import { getAvatar } from "./Avatars";
|
||||||
@@ -400,7 +401,7 @@ export function DagFlowMiniPanel({
|
|||||||
return (
|
return (
|
||||||
<MiniNode
|
<MiniNode
|
||||||
key={agentId}
|
key={agentId}
|
||||||
label={meta.name}
|
label={desensitize(meta.name)}
|
||||||
avatarType={meta.avatarType}
|
avatarType={meta.avatarType}
|
||||||
state={state}
|
state={state}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { getAvatar } from "./Avatars";
|
import { getAvatar } from "./Avatars";
|
||||||
@@ -236,7 +237,7 @@ export function DispatchChainModal({
|
|||||||
done={node.state === "done"}
|
done={node.state === "done"}
|
||||||
/>
|
/>
|
||||||
<FlowNode
|
<FlowNode
|
||||||
label={node.name}
|
label={desensitize(node.name)}
|
||||||
avatarType={node.avatarType}
|
avatarType={node.avatarType}
|
||||||
state={node.state}
|
state={node.state}
|
||||||
order={node.order}
|
order={node.order}
|
||||||
@@ -277,7 +278,7 @@ export function DispatchChainModal({
|
|||||||
<div key={d.id} className="flex gap-2.5">
|
<div key={d.id} className="flex gap-2.5">
|
||||||
<div className="shrink-0">{getAvatar(avatarForDialogue(d), "h-7 w-7")}</div>
|
<div className="shrink-0">{getAvatar(avatarForDialogue(d), "h-7 w-7")}</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="mb-0.5 text-xs font-semibold text-foreground/80">{d.name}</div>
|
<div className="mb-0.5 text-xs font-semibold text-foreground/80">{desensitize(d.name)}</div>
|
||||||
<div className="prose prose-sm max-w-none break-words text-sm leading-relaxed text-foreground/80 dark:prose-invert">
|
<div className="prose prose-sm max-w-none break-words text-sm leading-relaxed text-foreground/80 dark:prose-invert">
|
||||||
<MarkdownContent>{d.content || "…"}</MarkdownContent>
|
<MarkdownContent>{d.content || "…"}</MarkdownContent>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
type ThreadIdMap,
|
type ThreadIdMap,
|
||||||
} from "@/roundtable-planning/api/multi-agent";
|
} from "@/roundtable-planning/api/multi-agent";
|
||||||
import { ConsensusDocxDownloadButton } from "@/roundtable-planning/components/ConsensusDocxDownloadButton";
|
import { ConsensusDocxDownloadButton } from "@/roundtable-planning/components/ConsensusDocxDownloadButton";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||||
import {
|
import {
|
||||||
Step3ReportMode,
|
Step3ReportMode,
|
||||||
@@ -707,7 +708,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
lit ? "text-slate-800 dark:text-foreground" : "text-slate-400 dark:text-muted-foreground",
|
lit ? "text-slate-800 dark:text-foreground" : "text-slate-400 dark:text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{seat.displayName}
|
{desensitize(seat.displayName)}
|
||||||
</span>
|
</span>
|
||||||
<span className="block truncate text-[9.5px] font-mono text-slate-400 dark:text-muted-foreground">
|
<span className="block truncate text-[9.5px] font-mono text-slate-400 dark:text-muted-foreground">
|
||||||
{lit ? `${seat.delivery!.length} 字` : "无交付"}
|
{lit ? `${seat.delivery!.length} 字` : "无交付"}
|
||||||
@@ -771,7 +772,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
{consensusText ? (
|
{consensusText ? (
|
||||||
<div className="text-xs text-slate-700 leading-relaxed dark:text-foreground/80">
|
<div className="text-xs text-slate-700 leading-relaxed dark:text-foreground/80">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{consensusText}
|
{desensitize(consensusText)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -830,7 +831,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
{getAvatar(seat.avatarType, "h-5.5 w-5.5")}
|
{getAvatar(seat.avatarType, "h-5.5 w-5.5")}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-black text-slate-800 truncate dark:text-foreground">
|
<span className="text-xs font-black text-slate-800 truncate dark:text-foreground">
|
||||||
{seat.displayName}
|
{desensitize(seat.displayName)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-mono shrink-0 dark:text-muted-foreground">
|
<span className="text-[10px] text-slate-400 font-mono shrink-0 dark:text-muted-foreground">
|
||||||
@@ -858,7 +859,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
components={markdownComponents}
|
components={markdownComponents}
|
||||||
>
|
>
|
||||||
{seat.delivery!}
|
{desensitize(seat.delivery!)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[11px] text-slate-400 italic dark:text-muted-foreground">该席位暂无交付内容。</p>
|
<p className="text-[11px] text-slate-400 italic dark:text-muted-foreground">该席位暂无交付内容。</p>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import { BrainIcon, ChevronDownIcon, Loader2 } from "lucide-react";
|
import { BrainIcon, ChevronDownIcon, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface MessageBubbleProps {
|
export interface MessageBubbleProps {
|
||||||
@@ -58,7 +59,8 @@ export function MessageBubble({
|
|||||||
senderNameClassName,
|
senderNameClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{senderName}
|
{/* 智能体名称需脱敏(消息列表里唯一要脱敏的就是名称)。 */}
|
||||||
|
{desensitize(senderName)}
|
||||||
</span>
|
</span>
|
||||||
{badge && (
|
{badge && (
|
||||||
<span className="roundtable-chat-meta font-mono font-black uppercase text-[var(--brand-900)] bg-[var(--brand-200)] border border-red-200 px-1.5 py-0.2 rounded">
|
<span className="roundtable-chat-meta font-mono font-black uppercase text-[var(--brand-900)] bg-[var(--brand-200)] border border-red-200 px-1.5 py-0.2 rounded">
|
||||||
@@ -190,6 +192,7 @@ export const sharedMarkdownComponents = {
|
|||||||
* 便捷封装:用 sharedMarkdownComponents + remarkGfm 渲染 markdown。
|
* 便捷封装:用 sharedMarkdownComponents + remarkGfm 渲染 markdown。
|
||||||
* 大多数 caller 都不需要再覆盖任何 components,直接用这个就好。
|
* 大多数 caller 都不需要再覆盖任何 components,直接用这个就好。
|
||||||
*/
|
*/
|
||||||
|
// 消息列表正文**不脱敏**(仅名称脱敏);MarkdownContent 是会商对话气泡正文渲染器,故原文渲染。
|
||||||
export function MarkdownContent({ children }: { children: string }) {
|
export function MarkdownContent({ children }: { children: string }) {
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={sharedMarkdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={sharedMarkdownComponents}>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
import { CheckIcon, Loader2Icon, XIcon } from "lucide-react";
|
import { CheckIcon, Loader2Icon, XIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { getAvatar } from "@/roundtable-planning/components/Avatars";
|
import { getAvatar } from "@/roundtable-planning/components/Avatars";
|
||||||
import type { ParallelSeatView } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
import type { ParallelSeatView } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||||
@@ -42,7 +43,7 @@ export function ParallelSeatStrip({ batch, selectedSeats, onToggleSeat }: Parall
|
|||||||
key={seat.agentId}
|
key={seat.agentId}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onToggleSeat(seat.agentId)}
|
onClick={() => onToggleSeat(seat.agentId)}
|
||||||
title={`${seat.displayName}${isSelected ? "(主区显示中)" : "(点击在主区显示)"}`}
|
title={`${desensitize(seat.displayName)}${isSelected ? "(主区显示中)" : "(点击在主区显示)"}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative shrink-0 rounded-full transition",
|
"relative shrink-0 rounded-full transition",
|
||||||
isSelected ? "ring-2 ring-primary ring-offset-1" : "ring-1 ring-transparent hover:ring-slate-300",
|
isSelected ? "ring-2 ring-primary ring-offset-1" : "ring-1 ring-transparent hover:ring-slate-300",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { listAgents, type Agent } from "@/core/agents";
|
import { listAgents, type Agent } from "@/core/agents";
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1122,7 +1123,7 @@ function AgentCard({
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="font-medium text-sm truncate">{candidate.name || candidate.agent_id}</div>
|
<div className="font-medium text-sm truncate">{desensitize(candidate.name || candidate.agent_id)}</div>
|
||||||
{recommended && (
|
{recommended && (
|
||||||
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/10 text-purple-700 dark:text-purple-300 font-medium">
|
<span className="text-[10px] px-1.5 py-0.5 rounded bg-purple-500/10 text-purple-700 dark:text-purple-300 font-medium">
|
||||||
推荐
|
推荐
|
||||||
@@ -1131,12 +1132,12 @@ function AgentCard({
|
|||||||
</div>
|
</div>
|
||||||
{candidate.description && (
|
{candidate.description && (
|
||||||
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
<div className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
||||||
{candidate.description}
|
{desensitize(candidate.description)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{reason ? (
|
{reason ? (
|
||||||
<div className="text-xs text-primary/80 mt-1 italic">
|
<div className="text-xs text-primary/80 mt-1 italic">
|
||||||
<span className="font-medium not-italic">推荐理由:</span> {reason}
|
<span className="font-medium not-italic">推荐理由:</span> {desensitize(reason)}
|
||||||
</div>
|
</div>
|
||||||
) : streaming ? (
|
) : streaming ? (
|
||||||
<div className="text-xs text-muted-foreground mt-1 italic">正在生成推荐理由...</div>
|
<div className="text-xs text-muted-foreground mt-1 italic">正在生成推荐理由...</div>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToB
|
|||||||
import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll";
|
import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll";
|
||||||
import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics";
|
import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics";
|
||||||
import type { InteractiveMessage } from "@/roundtable-planning/hooks/useStep1Intent";
|
import type { InteractiveMessage } from "@/roundtable-planning/hooks/useStep1Intent";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
import { splitInlineReasoning } from "@/roundtable-planning/lib/reasoning";
|
import { splitInlineReasoning } from "@/roundtable-planning/lib/reasoning";
|
||||||
import { splitIntentReadyBlock } from "@/roundtable-planning/lib/roundtable-constants";
|
import { splitIntentReadyBlock } from "@/roundtable-planning/lib/roundtable-constants";
|
||||||
import type {
|
import type {
|
||||||
@@ -149,10 +150,13 @@ export function Step1Panel(props: Step1PanelProps) {
|
|||||||
setAttachedFiles([]);
|
setAttachedFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 顶部标题栏整片脱敏(消息列表正文在下方,不在此容器内,保持原文)。
|
||||||
|
const headerDzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
<div className="flex-1 flex flex-col min-h-0">
|
||||||
{/* Dialogue Header */}
|
{/* Dialogue Header */}
|
||||||
<div className="shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
<div ref={headerDzRef} className="shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-slate-50/50">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<MessageSquare className="w-4.5 h-4.5 text-primary" strokeWidth={2.5} />
|
<MessageSquare className="w-4.5 h-4.5 text-primary" strokeWidth={2.5} />
|
||||||
<h3 className="text-sm font-extrabold text-slate-900">
|
<h3 className="text-sm font-extrabold text-slate-900">
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ import {
|
|||||||
getAvatarTypeFor,
|
getAvatarTypeFor,
|
||||||
type RoleEntry,
|
type RoleEntry,
|
||||||
} from "@/roundtable-planning/lib/roundtable-constants";
|
} from "@/roundtable-planning/lib/roundtable-constants";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
import { splitInlineReasoning } from "@/roundtable-planning/lib/reasoning";
|
import { splitInlineReasoning } from "@/roundtable-planning/lib/reasoning";
|
||||||
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
|
import type { ClarificationPayload } from "@/roundtable-planning/api/multi-agent";
|
||||||
|
|
||||||
@@ -405,10 +407,13 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
isOrchestrating ||
|
isOrchestrating ||
|
||||||
step2RoundtableDialogues.some((dialogue) => dialogue.streaming === true);
|
step2RoundtableDialogues.some((dialogue) => dialogue.streaming === true);
|
||||||
|
|
||||||
|
// 顶部标题栏整片脱敏(消息列表正文不在此容器,保持原文)。
|
||||||
|
const headerDzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
|
|
||||||
const chatBody = (
|
const chatBody = (
|
||||||
<>
|
<>
|
||||||
{/* Headline */}
|
{/* Headline */}
|
||||||
<div className="shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-white shadow-sm">
|
<div ref={headerDzRef} className="shrink-0 px-6 py-4.5 border-b border-slate-100 flex items-center justify-between bg-white shadow-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Cpu className="w-5 h-5 text-primary" strokeWidth={2.5} />
|
<Cpu className="w-5 h-5 text-primary" strokeWidth={2.5} />
|
||||||
<div>
|
<div>
|
||||||
@@ -584,7 +589,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-6 text-center text-xs text-slate-400">
|
<div className="py-6 text-center text-xs text-slate-400">
|
||||||
{seat.displayName} · 等待输出…
|
{desensitize(seat.displayName)} · 等待输出…
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -599,7 +604,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
<span className="roundtable-chat-meta font-medium text-slate-600">
|
<span className="roundtable-chat-meta font-medium text-slate-600">
|
||||||
仅显示{" "}
|
仅显示{" "}
|
||||||
<span className="font-bold text-primary">
|
<span className="font-bold text-primary">
|
||||||
{seatFilterLabel(seatFilter)}
|
{desensitize(seatFilterLabel(seatFilter))}
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
的发言
|
的发言
|
||||||
</span>
|
</span>
|
||||||
@@ -615,7 +620,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
|||||||
)}
|
)}
|
||||||
{seatFilter && visibleDialogues.length === 0 ? (
|
{seatFilter && visibleDialogues.length === 0 ? (
|
||||||
<div className="py-10 text-center text-xs text-slate-400">
|
<div className="py-10 text-center text-xs text-slate-400">
|
||||||
没有匹配「{seatFilterLabel(seatFilter)}」的消息
|
没有匹配「{desensitize(seatFilterLabel(seatFilter))}」的消息
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
visibleDialogues.map((msg) => (
|
visibleDialogues.map((msg) => (
|
||||||
@@ -878,7 +883,7 @@ export function Step2Message({
|
|||||||
title={
|
title={
|
||||||
isAvatarSelected
|
isAvatarSelected
|
||||||
? "取消筛选,显示全部发言"
|
? "取消筛选,显示全部发言"
|
||||||
: `只看 ${msg.sender} 的发言`
|
: `只看 ${desensitize(msg.sender)} 的发言`
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 cursor-pointer rounded-full transition hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60",
|
"shrink-0 cursor-pointer rounded-full transition hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60",
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export function Step2SandboxLayout({
|
|||||||
threadId={previewThreadId}
|
threadId={previewThreadId}
|
||||||
onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined}
|
onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined}
|
||||||
allowVirtualMarkdownEdit
|
allowVirtualMarkdownEdit
|
||||||
|
// 会商「消息列表沙箱」按需求不脱敏产物内容(用户要拿原始文件)。
|
||||||
|
disableDesensitize
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex size-full justify-center">
|
<div className="relative flex size-full justify-center">
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
|
import { desensitizeDeep } from "@/lib/desensitize";
|
||||||
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
||||||
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp";
|
||||||
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
|
import { adaptReportData } from "@/roundtable-planning/dashboard/data-context";
|
||||||
@@ -101,10 +102,12 @@ export function Step3DashboardMode({
|
|||||||
} = dashboard;
|
} = dashboard;
|
||||||
|
|
||||||
// 真实数据(report-json):优先本轮实时生成,其次草稿快照。应用渲染/网页渲染共用。
|
// 真实数据(report-json):优先本轮实时生成,其次草稿快照。应用渲染/网页渲染共用。
|
||||||
const report = useMemo(
|
// 整体深度脱敏后再渲染——大屏(DashboardApp)与自包含 HTML(iframe/新窗口/分享/下载)
|
||||||
() => dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null),
|
// 都从这份 report 派生,故在此一处脱敏即覆盖大屏的所有展示文本(会商页非沙箱内容需脱敏)。
|
||||||
[dashboard.data, savedReport?.dashboardJson],
|
const report = useMemo(() => {
|
||||||
);
|
const raw = dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null);
|
||||||
|
return raw ? desensitizeDeep(raw) : raw;
|
||||||
|
}, [dashboard.data, savedReport?.dashboardJson]);
|
||||||
|
|
||||||
// 应用渲染用的大屏数据(不传则 DashboardApp 回落 DEMO)。
|
// 应用渲染用的大屏数据(不传则 DashboardApp 回落 DEMO)。
|
||||||
const dashboardData = useMemo(
|
const dashboardData = useMemo(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { ConsensusDocxDownloadButton } from "@/roundtable-planning/components/Co
|
|||||||
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
||||||
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
||||||
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||||
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
||||||
import { Step3DashboardMode } from "@/roundtable-planning/components/Step3DashboardMode";
|
import { Step3DashboardMode } from "@/roundtable-planning/components/Step3DashboardMode";
|
||||||
@@ -407,7 +408,7 @@ export function Step3ReportMode({
|
|||||||
>
|
>
|
||||||
{consensusText ? (
|
{consensusText ? (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{consensusText}
|
{desensitize(consensusText)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-slate-400 italic">暂无总控结论。</p>
|
<p className="text-xs text-slate-400 italic">暂无总控结论。</p>
|
||||||
@@ -773,14 +774,14 @@ function SeatDeliveriesView({ seatDeliveries }: { seatDeliveries: SeatDelivery[]
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<ListOrdered className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
<ListOrdered className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||||
<span className="text-[11.5px] font-bold text-slate-800 truncate">{seat.sender}</span>
|
<span className="text-[11.5px] font-bold text-slate-800 truncate">{desensitize(seat.sender)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-mono shrink-0">{seat.content.length} 字</span>
|
<span className="text-[10px] text-slate-400 font-mono shrink-0">{seat.content.length} 字</span>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="px-4 py-3 text-xs text-slate-700 leading-relaxed border-t border-slate-100 bg-white">
|
<div className="px-4 py-3 text-xs text-slate-700 leading-relaxed border-t border-slate-100 bg-white">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{seat.content || "(空)"}
|
{desensitize(seat.content || "(空)")}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
||||||
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
||||||
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||||
import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMode";
|
import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMode";
|
||||||
import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts";
|
import type { DraftStep3SummarySnapshot } from "@/roundtable-planning/api/drafts";
|
||||||
@@ -449,7 +450,7 @@ export function Step3SummaryMode({
|
|||||||
>
|
>
|
||||||
{consensusText ? (
|
{consensusText ? (
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{consensusText}
|
{desensitize(consensusText)}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-slate-400 italic">暂无总控结论。</p>
|
<p className="text-xs text-slate-400 italic">暂无总控结论。</p>
|
||||||
@@ -846,14 +847,14 @@ function SeatDeliveriesView({ seatDeliveries }: { seatDeliveries: SeatDelivery[]
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
<FileText className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
<FileText className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||||
<span className="text-[11.5px] font-bold text-slate-800 truncate">{seat.sender}</span>
|
<span className="text-[11.5px] font-bold text-slate-800 truncate">{desensitize(seat.sender)}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-400 font-mono shrink-0">{seat.content.length} 字</span>
|
<span className="text-[10px] text-slate-400 font-mono shrink-0">{seat.content.length} 字</span>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="px-4 py-3 text-xs text-slate-700 leading-relaxed border-t border-slate-100 bg-white">
|
<div className="px-4 py-3 text-xs text-slate-700 leading-relaxed border-t border-slate-100 bg-white">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
|
||||||
{seat.content || "(空)"}
|
{desensitize(seat.content || "(空)")}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
|
|
||||||
import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-thought";
|
import { ChainOfThoughtSearchResult } from "@/components/ai-elements/chain-of-thought";
|
||||||
import { CodeBlock } from "@/components/ai-elements/code-block";
|
import { CodeBlock } from "@/components/ai-elements/code-block";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { MarkdownContent } from "@/roundtable-planning/components/MessageBubble";
|
import { MarkdownContent } from "@/roundtable-planning/components/MessageBubble";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,7 @@ export function renderStepArgs(
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{displayName && (
|
{displayName && (
|
||||||
<ChainOfThoughtSearchResult>{displayName}</ChainOfThoughtSearchResult>
|
<ChainOfThoughtSearchResult>{desensitize(displayName)}</ChainOfThoughtSearchResult>
|
||||||
)}
|
)}
|
||||||
{task && (
|
{task && (
|
||||||
<div className="roundtable-chat-message mt-1 max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white px-2.5 py-1 text-slate-600">
|
<div className="roundtable-chat-message mt-1 max-h-64 overflow-y-auto rounded-md border border-slate-200 bg-white px-2.5 py-1 text-slate-600">
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* 区域级脱敏 Hook —— 把某个容器(DOM 子树)里的**所有文本节点**做脱敏,
|
||||||
|
* 用于「左/右侧面板 + 顶部标题」这类需要整片脱敏的区域:静态标题、动态内容、
|
||||||
|
* 嵌套子组件文本一并覆盖,无需逐串包 `desensitize()`(那样易漏、难维护)。
|
||||||
|
*
|
||||||
|
* 用法:`const ref = useDesensitizeSubtree<HTMLDivElement>(); <section ref={ref}>…</section>`。
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
|
* - 只读改文本节点(display-only),不碰 `<input>/<textarea>` 的 value、不改 React 状态/回传值;
|
||||||
|
* - `desensitize` 幂等(替换词不再命中词表),MutationObserver 重入安全;改写前先 disconnect 防自触发循环;
|
||||||
|
* - 想在区域内**保留原文**(如内嵌沙箱/消息列表),给该子树根加 `data-no-desensitize` 属性即可跳过。
|
||||||
|
*
|
||||||
|
* 注意:**不要**用它包「消息列表正文」或「沙箱产物预览」——这两处按需求保持原文,
|
||||||
|
* 应作为独立容器、不挂本 Hook(智能体名称的脱敏单独在 MessageBubble 的 senderName 处理)。
|
||||||
|
*/
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
|
|
||||||
|
const _SKIP_TAGS = new Set(["SCRIPT", "STYLE", "TEXTAREA", "INPUT", "IFRAME", "CODE", "PRE"]);
|
||||||
|
|
||||||
|
export function useDesensitizeSubtree<T extends HTMLElement>() {
|
||||||
|
const ref = useRef<T | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = ref.current;
|
||||||
|
if (!root || typeof MutationObserver === "undefined") return;
|
||||||
|
|
||||||
|
let observer: MutationObserver | null = null;
|
||||||
|
|
||||||
|
const apply = () => {
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
|
||||||
|
acceptNode(node) {
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (!parent) return NodeFilter.FILTER_REJECT;
|
||||||
|
if (_SKIP_TAGS.has(parent.tagName)) return NodeFilter.FILTER_REJECT;
|
||||||
|
// 显式标记 data-no-desensitize 的子树保留原文(如内嵌沙箱)。
|
||||||
|
if (parent.closest("[data-no-desensitize]")) return NodeFilter.FILTER_REJECT;
|
||||||
|
return NodeFilter.FILTER_ACCEPT;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const edits: Array<[Text, string]> = [];
|
||||||
|
let current = walker.nextNode();
|
||||||
|
while (current) {
|
||||||
|
const textNode = current as Text;
|
||||||
|
const original = textNode.nodeValue ?? "";
|
||||||
|
if (original.trim()) {
|
||||||
|
const masked = desensitize(original);
|
||||||
|
if (masked !== original) edits.push([textNode, masked]);
|
||||||
|
}
|
||||||
|
current = walker.nextNode();
|
||||||
|
}
|
||||||
|
if (edits.length === 0) return;
|
||||||
|
// 改写期间停掉观察,避免自身改动再次触发,改完恢复。
|
||||||
|
observer?.disconnect();
|
||||||
|
for (const [textNode, masked] of edits) textNode.nodeValue = masked;
|
||||||
|
observer?.observe(root, { subtree: true, childList: true, characterData: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
apply();
|
||||||
|
observer = new MutationObserver(() => apply());
|
||||||
|
observer.observe(root, { subtree: true, childList: true, characterData: true });
|
||||||
|
return () => observer?.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -63,6 +63,7 @@ import {
|
|||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||||
import { listAgents } from "@/core/agents";
|
import { listAgents } from "@/core/agents";
|
||||||
|
|
||||||
@@ -134,6 +135,8 @@ type ActiveDrag =
|
|||||||
export default function BusinessChainEditorPage() {
|
export default function BusinessChainEditorPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
// 整片脱敏:链条配置页展示文本(链条名/描述/席位名/标题)。可编辑的 input/textarea 不脱敏(Hook 跳过),保证编辑用原文。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
|
|
||||||
// ─── 数据 ────────────────────────────────────────────────────────────
|
// ─── 数据 ────────────────────────────────────────────────────────────
|
||||||
const [pool, setPool] = useState<RecommendCandidate[]>([]);
|
const [pool, setPool] = useState<RecommendCandidate[]>([]);
|
||||||
@@ -804,7 +807,7 @@ export default function BusinessChainEditorPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col bg-muted/40 text-foreground">
|
<div ref={dzRef} className="flex h-full min-h-0 flex-col bg-muted/40 text-foreground">
|
||||||
{/* 顶栏 */}
|
{/* 顶栏 */}
|
||||||
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b border-border bg-card px-6 py-3">
|
<header className="flex shrink-0 flex-wrap items-center gap-3 border-b border-border bg-card px-6 py-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useRouteNavigate } from "@/core/route-base";
|
import { useRouteNavigate } from "@/core/route-base";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
|
|
||||||
import { deleteChain, listChains, setChainPublic, type ChainScope } from "../api/chains";
|
import { deleteChain, listChains, setChainPublic, type ChainScope } from "../api/chains";
|
||||||
import { chainTagClasses, type BusinessChain } from "../lib/business-chain";
|
import { chainTagClasses, type BusinessChain } from "../lib/business-chain";
|
||||||
@@ -79,6 +80,8 @@ const BRAND =
|
|||||||
|
|
||||||
export default function BusinessChainListPage() {
|
export default function BusinessChainListPage() {
|
||||||
const navigate = useRouteNavigate();
|
const navigate = useRouteNavigate();
|
||||||
|
// 整片脱敏:链条列表页展示文本(链条名/描述/席位名/标题)。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLDivElement>();
|
||||||
|
|
||||||
// 进入编辑器:带 chainId = 打开并切到该链条;不带 = 空白新建。
|
// 进入编辑器:带 chainId = 打开并切到该链条;不带 = 空白新建。
|
||||||
// 路径写 workspace base,`useRouteNavigate` 会按当前路由 base 改写。
|
// 路径写 workspace base,`useRouteNavigate` 会按当前路由 base 改写。
|
||||||
@@ -233,7 +236,7 @@ export default function BusinessChainListPage() {
|
|||||||
}, [chains, search, sortBy]);
|
}, [chains, search, sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 w-full flex-col bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
<div ref={dzRef} className="flex h-full min-h-0 w-full flex-col bg-zinc-50/60 font-sans text-zinc-900 antialiased dark:bg-[#0C1836] dark:text-[#C5CFE0]">
|
||||||
{/* 顶栏:标题 + 描述 + 新建链条 */}
|
{/* 顶栏:标题 + 描述 + 新建链条 */}
|
||||||
<header className="shrink-0 border-b border-zinc-200/80 bg-white/85 px-6 py-4 backdrop-blur-md dark:border-[#1A2F5A] dark:bg-[#122347]/80">
|
<header className="shrink-0 border-b border-zinc-200/80 bg-white/85 px-6 py-4 backdrop-blur-md dark:border-[#1A2F5A] dark:bg-[#122347]/80">
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ import {
|
|||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||||
import { useSidebarSafe } from "@/components/ui/sidebar";
|
import { useSidebarSafe } from "@/components/ui/sidebar";
|
||||||
|
import { desensitize } from "@/lib/desensitize";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDesensitizeSubtree } from "@/roundtable-planning/lib/use-desensitize-subtree";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
|
|
||||||
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
import { getAvatar, Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
||||||
@@ -1178,6 +1180,8 @@ export default function RoundtablePlanningPage() {
|
|||||||
// 真正建好的窗口里,用它让历史记录显示「挂起中」(作业建好后转 queued/running,由后端状态接管)。
|
// 真正建好的窗口里,用它让历史记录显示「挂起中」(作业建好后转 queued/running,由后端状态接管)。
|
||||||
// resetToStep1 不卸载本页,故该 state 跨「跳转」存活。
|
// resetToStep1 不卸载本页,故该 state 跨「跳转」存活。
|
||||||
const [pendingSuspendIds, setPendingSuspendIds] = useState<Set<string>>(() => new Set());
|
const [pendingSuspendIds, setPendingSuspendIds] = useState<Set<string>>(() => new Set());
|
||||||
|
// 整片脱敏:顶部页头(步骤导航 / 历史记录 / 标题等)的所有文本。
|
||||||
|
const headerDzRef = useDesensitizeSubtree<HTMLElement>();
|
||||||
const markPendingSuspend = useCallback((draftId: string) => {
|
const markPendingSuspend = useCallback((draftId: string) => {
|
||||||
setPendingSuspendIds((prev) => {
|
setPendingSuspendIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1477,7 +1481,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header className="relative z-[40] flex shrink-0 items-center justify-between gap-4 border-b border-slate-100 bg-white px-6 py-3 shadow-[0_1px_3px_rgba(0,0,0,0.02)] dark:border-border dark:bg-card dark:shadow-none">
|
<header ref={headerDzRef} className="relative z-[40] flex shrink-0 items-center justify-between gap-4 border-b border-slate-100 bg-white px-6 py-3 shadow-[0_1px_3px_rgba(0,0,0,0.02)] dark:border-border dark:bg-card dark:shadow-none">
|
||||||
<StepNavigation
|
<StepNavigation
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
intentReady={step1.intentReady}
|
intentReady={step1.intentReady}
|
||||||
@@ -2141,6 +2145,9 @@ function LeftSidebar({
|
|||||||
onSelectChain,
|
onSelectChain,
|
||||||
onOpenDraft,
|
onOpenDraft,
|
||||||
}: LeftSidebarProps) {
|
}: LeftSidebarProps) {
|
||||||
|
// 整片脱敏:左侧面板(任务概览/参与角色/人工参与等)的所有文本——标题、动态内容、
|
||||||
|
// 嵌套子组件文本一并脱敏。消息列表与沙箱是另外的容器,不挂此 Hook,保持原文。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLElement>();
|
||||||
const isStep2 = currentStep === 2;
|
const isStep2 = currentStep === 2;
|
||||||
// 仅第二步可按席位筛选会商记录;点中已选则取消(回到显示全部)。
|
// 仅第二步可按席位筛选会商记录;点中已选则取消(回到显示全部)。
|
||||||
const toggleSeatFilter = (filter: SeatFilter) =>
|
const toggleSeatFilter = (filter: SeatFilter) =>
|
||||||
@@ -2152,6 +2159,7 @@ function LeftSidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
ref={dzRef}
|
||||||
id="left-sidebar"
|
id="left-sidebar"
|
||||||
className={cn(
|
className={cn(
|
||||||
"shrink-0 flex flex-col gap-3 overflow-y-auto pr-1 transition-[width] duration-200 ease-out",
|
"shrink-0 flex flex-col gap-3 overflow-y-auto pr-1 transition-[width] duration-200 ease-out",
|
||||||
@@ -3031,14 +3039,14 @@ function RoleAvatarBadge({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!clickable) {
|
if (!clickable) {
|
||||||
return <div title={`${role.name} · ${role.status}`}>{inner}</div>;
|
return <div title={`${desensitize(role.name)} · ${role.status}`}>{inner}</div>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
title={active ? "取消筛选,显示全部发言" : `只看 ${role.name} 的发言`}
|
title={active ? "取消筛选,显示全部发言" : `只看 ${desensitize(role.name)} 的发言`}
|
||||||
className="shrink-0 cursor-pointer rounded-full transition hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
className="shrink-0 cursor-pointer rounded-full transition hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/60"
|
||||||
>
|
>
|
||||||
{inner}
|
{inner}
|
||||||
@@ -3078,7 +3086,7 @@ function RoleRow({
|
|||||||
clickable
|
clickable
|
||||||
? active
|
? active
|
||||||
? "取消筛选,显示全部发言"
|
? "取消筛选,显示全部发言"
|
||||||
: `只看 ${role.name} 的发言`
|
: `只看 ${desensitize(role.name)} 的发言`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
@@ -3108,7 +3116,7 @@ function RoleRow({
|
|||||||
<RoleAvatarBadge role={role} agentNameToRole={agentNameToRole} size="h-10 w-10" />
|
<RoleAvatarBadge role={role} agentNameToRole={agentNameToRole} size="h-10 w-10" />
|
||||||
<div className="flex min-w-0 flex-col gap-0.5">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className="truncate text-sm font-bold leading-5 tracking-tight text-slate-800">
|
<span className="truncate text-sm font-bold leading-5 tracking-tight text-slate-800">
|
||||||
{role.name}
|
{desensitize(role.name)}
|
||||||
</span>
|
</span>
|
||||||
{role.type === "agent" && (
|
{role.type === "agent" && (
|
||||||
<span className="text-[11px] text-primary font-medium leading-4">AI 智能体</span>
|
<span className="text-[11px] text-primary font-medium leading-4">AI 智能体</span>
|
||||||
@@ -3178,10 +3186,13 @@ function RightSidebar({
|
|||||||
triggerToast,
|
triggerToast,
|
||||||
onSaveIntent,
|
onSaveIntent,
|
||||||
}: RightSidebarProps) {
|
}: RightSidebarProps) {
|
||||||
|
// 整片脱敏:右侧面板(推荐模型 / 意图摘要 / 澄清记录)所有文本。
|
||||||
|
const dzRef = useDesensitizeSubtree<HTMLElement>();
|
||||||
// Step 2 侧栏内容已迁至左侧;Step 3 沿用原逻辑不展示右侧栏。
|
// Step 2 侧栏内容已迁至左侧;Step 3 沿用原逻辑不展示右侧栏。
|
||||||
if (currentStep !== 1) return null;
|
if (currentStep !== 1) return null;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
ref={dzRef}
|
||||||
id="right-sidebar"
|
id="right-sidebar"
|
||||||
className="w-[300px] shrink-0 flex flex-col gap-4 overflow-y-auto pl-1"
|
className="w-[300px] shrink-0 flex flex-col gap-4 overflow-y-auto pl-1"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in new issue
Block a user