diff --git a/frontend-web/src/assets/img/layout-bg-theme-dark-blue.svg b/frontend-web/src/assets/img/layout-bg-theme-dark-blue.svg new file mode 100644 index 00000000..dabbcbe3 --- /dev/null +++ b/frontend-web/src/assets/img/layout-bg-theme-dark-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/img/nav-active.svg b/frontend-web/src/assets/img/nav-active.svg new file mode 100644 index 00000000..fe2b1952 --- /dev/null +++ b/frontend-web/src/assets/img/nav-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/img/top-bg.svg b/frontend-web/src/assets/img/top-bg.svg new file mode 100644 index 00000000..54563425 --- /dev/null +++ b/frontend-web/src/assets/img/top-bg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/img/菜单按钮.svg b/frontend-web/src/assets/img/菜单按钮.svg new file mode 100644 index 00000000..a7062876 --- /dev/null +++ b/frontend-web/src/assets/img/菜单按钮.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index d2e9f08a..7e22e50c 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -379,6 +379,13 @@ overflow-y: hidden; } +/* 左右分组:不撑满、不裁切选中高光 */ +.top-submenu-bar--group { + flex: 0 0 auto; + margin-left: 0; + overflow: visible; +} + .top-submenu-list { min-width: max-content; height: 64px; @@ -398,48 +405,31 @@ border: 0; padding: 0 18px; background: transparent; - color: rgba(224, 242, 255, 0.9); + color: rgba(255, 255, 255, 0.8); font-family: "Source Han Sans SC", "Noto Sans SC", "Noto Sans S Chinese", "Microsoft YaHei", sans-serif; - font-size: 16px; + font-size: 18px; line-height: 1; font-weight: 700; letter-spacing: 0; white-space: nowrap; cursor: pointer; - overflow: hidden; - transition: color 0.18s ease, text-shadow 0.18s ease, background 0.18s ease; + overflow: visible; + transition: color 0.18s ease, text-shadow 0.18s ease; } -.top-submenu-item:hover, +.top-submenu-item:hover { + color: #9fe3ff; + text-shadow: 0 0 10px rgba(91, 210, 255, 0.4); +} + +/* 选中态:设计稿提供的 nav-active.svg 发光高光 + 青色文字 */ .top-submenu-item--active { - color: #ffffff; - text-shadow: 0 0 10px rgba(91, 210, 255, 0.45); - background: - linear-gradient(135deg, rgba(15, 84, 169, 0.52) 0%, rgba(5, 43, 103, 0.34) 76%), - linear-gradient(180deg, rgba(39, 126, 230, 0.2) 0%, rgba(0, 50, 122, 0.26) 100%); - box-shadow: inset 0 1px 0 rgba(98, 180, 255, 0.16), inset 0 -18px 22px rgba(0, 31, 79, 0.24); -} - -.top-submenu-item--active::before { - content: ''; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 2px; - background: linear-gradient(90deg, rgba(59, 162, 255, 0) 0%, #35d7ff 50%, rgba(59, 162, 255, 0) 100%); - box-shadow: 0 0 12px rgba(53, 215, 255, 0.9), 0 -6px 18px rgba(33, 143, 255, 0.36); -} - -.top-submenu-item--active::after { - content: ''; - position: absolute; - left: 16px; - right: 16px; - bottom: 0; - height: 16px; - background: radial-gradient(ellipse at bottom, rgba(37, 189, 255, 0.34) 0%, rgba(37, 189, 255, 0) 68%); - pointer-events: none; + color: #64cdf5; + text-shadow: 0 0 12px rgba(100, 205, 245, 0.5); + background-image: url("../assets/img/nav-active.svg"); + background-repeat: no-repeat; + background-position: center calc(50% + 7px); + background-size: 152px 42px; } .third-submenu-bar { diff --git a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx index fb60500f..83c984c5 100644 --- a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx @@ -1,7 +1,8 @@ "use client"; -import { DropletIcon, LayoutGridIcon, ListIcon, MoonIcon, SunIcon } from "lucide-react"; +import { DropletIcon, LayoutGridIcon, LayoutIcon, ListIcon, MoonIcon, PanelLeftIcon, SunIcon } from "lucide-react"; import { useMemo, type ComponentType, type SVGProps } from "react"; +import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { useTheme } from "@/contexts/ThemeContext"; @@ -42,9 +43,10 @@ const languageOptions: { value: Locale; label: string }[] = [ { value: "zh-CN", label: zhCN.locale.localName }, ]; -export function AppearanceSettingsPage() { +export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const { t, locale, changeLocale } = useI18n(); const { theme, setTheme } = useTheme(); + const navigate = useNavigate(); const currentTheme = theme; const [settings, setSettings] = useLocalSettings(); const isAdmin = getStoredAuth()?.system_role === "admin"; @@ -145,6 +147,42 @@ export function AppearanceSettingsPage() { + + + +
+ + +
+
+ {isAdmin && ( <> diff --git a/frontend-web/src/components/workspace/settings/settings-dialog.tsx b/frontend-web/src/components/workspace/settings/settings-dialog.tsx index ea5cc6a6..00f587dd 100644 --- a/frontend-web/src/components/workspace/settings/settings-dialog.tsx +++ b/frontend-web/src/components/workspace/settings/settings-dialog.tsx @@ -121,7 +121,9 @@ export function SettingsDialog(props: SettingsDialogProps) {
{activeSection === "account" && } - {activeSection === "appearance" && } + {activeSection === "appearance" && ( + props.onOpenChange?.(false)} /> + )} {activeSection === "memory" && } {activeSection === "tools" && } {activeSection === "about" && } diff --git a/frontend-web/src/core/auth/index.ts b/frontend-web/src/core/auth/index.ts index 95923851..88cf4259 100644 --- a/frontend-web/src/core/auth/index.ts +++ b/frontend-web/src/core/auth/index.ts @@ -44,6 +44,37 @@ export function clearStoredAuth() { window.localStorage.removeItem(AUTH_STORAGE_KEY); } +/** + * 把登录用户名写入 localStorage.userInfo.userName(顶栏用户区据此显示)。 + * 仅在尚未设置时写入,避免覆盖外部登录(yUserId)从 consumer 拿到的用户名。 + */ +export function setStoredUserName(name: string | null | undefined) { + if (!isBrowser()) { + return; + } + const trimmed = name?.trim(); + if (!trimmed) { + return; + } + let info: Record = {}; + try { + const raw = window.localStorage.getItem("userInfo"); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + info = parsed as Record; + } + } + } catch { + info = {}; + } + if (typeof info.userName === "string" && info.userName.trim()) { + return; + } + info.userName = trimmed; + window.localStorage.setItem("userInfo", JSON.stringify(info)); +} + export function getAccessToken(): string | undefined { const auth = getStoredAuth(); const token = auth?.access_token?.trim(); diff --git a/frontend-web/src/core/settings/local.ts b/frontend-web/src/core/settings/local.ts index 1c7555ac..cb78eba0 100644 --- a/frontend-web/src/core/settings/local.ts +++ b/frontend-web/src/core/settings/local.ts @@ -62,6 +62,11 @@ export interface LocalSettings { */ skillViewMode?: "card" | "list"; agentViewMode?: "card" | "list"; + /** + * Layout mode: "full" shows the page sidebar + header; "compact" hides them + * for a cleaner work area. Defaults to "full" (undefined treated as "full"). + */ + layoutMode?: "full" | "compact"; }; context: Omit< AgentThreadContext, diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index fa3a806b..e6ca5d91 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -47,7 +47,7 @@ const MATERIAL_SOURCE_OPTIONS: Array<{ value: MaterialSource; label: string; des { value: 'notebook', label: '我的空间', desc: '从空间中检索素材' }, ]; -const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222]'; +const FIELD_LABEL_CLASS = 'text-[16px] font-semibold leading-none text-[#222] dark:text-[#AACEFF]'; const FIELD_TRIGGER_CLASS = 'h-12 w-full rounded-xl border-[#e8dedd] bg-white text-sm shadow-sm'; diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx index 983eec6d..7663204d 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingPanel.tsx @@ -254,7 +254,7 @@ export function AIWritingPanel({ const showNewButton = isHistoryMode || state.status !== 'idle'; return ( -
+
{!isHistoryMode && ( )} diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx index 566861e9..b1207024 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSetupChat.tsx @@ -114,7 +114,7 @@ export function WritingSetupChat() { return ( -
+
{/* 空状态隐藏对话头部,直接展示欢迎屏(与原型图一致);有消息后再显示头部 */} {!showWelcome && (
@@ -139,7 +139,7 @@ export function WritingSetupChat() { )}
-
+
{/* 与原型图一致:白色圆角卡片包裹「模型选择行 + 输入框」 */}
diff --git a/frontend-web/src/open-canvas/contexts/WritingFormContext.tsx b/frontend-web/src/open-canvas/contexts/WritingFormContext.tsx index d2d35298..a5bfbe8c 100644 --- a/frontend-web/src/open-canvas/contexts/WritingFormContext.tsx +++ b/frontend-web/src/open-canvas/contexts/WritingFormContext.tsx @@ -59,7 +59,7 @@ export function useWritingFormOptional(): WritingFormValue | null { export function WritingFormProvider({ children, initialMaterialSource }: { children: ReactNode; initialMaterialSource?: 'general' | 'notebook' }) { const [intent, setIntent] = useState(''); const [formExpanded, setFormExpanded] = useState(false); - const [showOutline, setShowOutline] = useState(false); + const [showOutline, setShowOutline] = useState(true); const [outlineText, setOutlineText] = useState(''); const [articleType, setArticleType] = useState('analysis'); const [wordCountInput, setWordCountInput] = useState('800'); diff --git a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx index 9160519d..2e86de1c 100644 --- a/frontend-web/src/open-canvas/pages/AIWritingPage.tsx +++ b/frontend-web/src/open-canvas/pages/AIWritingPage.tsx @@ -229,7 +229,7 @@ export default function AIWritingPage({ -
+
diff --git a/frontend-web/src/pages/AgentChatPage.tsx b/frontend-web/src/pages/AgentChatPage.tsx index 91c61f84..33626595 100644 --- a/frontend-web/src/pages/AgentChatPage.tsx +++ b/frontend-web/src/pages/AgentChatPage.tsx @@ -180,7 +180,7 @@ export default function AgentChatPage() { separate flex sibling OUTSIDE ChatBox, so the artifact preview always opens between the chat and the detail panel — the detail panel stays rightmost. */} -
+
{/* Main conversation column. `relative` hosts the floating diff --git a/frontend-web/src/pages/BusinessChainMappingPage.tsx b/frontend-web/src/pages/BusinessChainMappingPage.tsx index ae2db62d..291f3cdd 100644 --- a/frontend-web/src/pages/BusinessChainMappingPage.tsx +++ b/frontend-web/src/pages/BusinessChainMappingPage.tsx @@ -1,14 +1,23 @@ /** - * 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。 + * 配置管理页 —— 把固定业务码绑定到「智能体」+「业务链」。 * - * 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉: - * - 智能体来自 `/api/agents` - * - 业务链来自 `/api/roundtable-chains?scope=public`(只列公共链,保证全局共享时各账号都能解析) + * 智能体来自 `/api/agents`,业务链来自 `/api/roundtable-chains?scope=public`。 * 保存调 `PUT /api/business-mapping/{code}`。映射全局共享、无用户隔离。 */ import { useEffect, useMemo, useState } from "react"; -import { Loading, Select } from "tdesign-react"; -import { Link2, Pencil, Save, Users, X } from "lucide-react"; +import { motion } from "motion/react"; +import { Select } from "tdesign-react"; +import { + AlertCircle, + HelpCircle, + Info, + Link2, + Pencil, + Save, + Search, + Users, + X, +} from "lucide-react"; import { toast } from "sonner"; import { useTheme } from "@/contexts/ThemeContext"; @@ -21,11 +30,7 @@ import { updateBusinessMapping, type BusinessMapping, } from "@/strategy-components/api/business-mapping"; - -interface DraftSelection { - agentId: string | null; - chainId: string | null; -} +import { cn } from "@/lib/utils"; export default function BusinessChainMappingPage() { const { theme } = useTheme(); @@ -36,8 +41,13 @@ export default function BusinessChainMappingPage() { const [chains, setChains] = useState([]); const [loading, setLoading] = useState(true); const [editingCode, setEditingCode] = useState(null); - const [draft, setDraft] = useState({ agentId: null, chainId: null }); + const [draft, setDraft] = useState<{ agentId: string | null; chainId: string | null }>({ + agentId: null, + chainId: null, + }); const [saving, setSaving] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState<"all" | "configured" | "pending">("all"); useEffect(() => { let cancelled = false; @@ -58,9 +68,7 @@ export default function BusinessChainMappingPage() { if (!cancelled) setLoading(false); } })(); - return () => { - cancelled = true; - }; + return () => { cancelled = true; }; }, []); const agentOptions = useMemo( @@ -72,6 +80,24 @@ export default function BusinessChainMappingPage() { [chains], ); + const stats = useMemo(() => { + const total = rows.length; + const boundAgents = rows.filter((r) => r.agentId !== null).length; + return { total, boundAgents }; + }, [rows]); + + const filteredRows = useMemo(() => { + return rows.filter((r) => { + const q = searchQuery.toLowerCase(); + const matchesSearch = + r.label.toLowerCase().includes(q) || r.businessCode.toLowerCase().includes(q); + const isConfigured = r.agentId !== null && r.chainId !== null; + if (statusFilter === "configured") return matchesSearch && isConfigured; + if (statusFilter === "pending") return matchesSearch && !isConfigured; + return matchesSearch; + }); + }, [rows, searchQuery, statusFilter]); + const beginEdit = (row: BusinessMapping) => { setEditingCode(row.businessCode); setDraft({ agentId: row.agentId, chainId: row.chainId }); @@ -86,10 +112,10 @@ export default function BusinessChainMappingPage() { setSaving(true); try { const agentName = draft.agentId - ? agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null + ? (agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null) : null; const chainTitle = draft.chainId - ? chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null + ? (chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null) : null; const updated = await updateBusinessMapping(row.businessCode, { agentId: draft.agentId, @@ -111,162 +137,416 @@ export default function BusinessChainMappingPage() { } }; + /* ── 样式 ── */ const shellClass = isDark - ? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100" - : "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]"; + ? "h-full overflow-y-auto text-slate-100 workspace-chat-bg" + : "h-full overflow-y-auto bg-white text-[#222]"; + + // 卡片:无边框,纯阴影 const cardClass = isDark - ? "border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] shadow-[0_16px_36px_rgba(3,10,22,0.2)]" - : "border border-[#E8E8E8] bg-white shadow-[0_14px_30px_rgba(245,34,45,0.06)]"; - const headRowClass = isDark ? "text-slate-400" : "text-[#8C8C8C]"; - const rowBorder = isDark ? "border-[rgba(67,89,124,0.32)]" : "border-[#F0F0F0]"; + ? "bg-[rgba(12,29,54,0.88)] shadow-[0_8px_32px_rgba(3,10,22,0.28)] backdrop-blur-sm" + : "bg-white shadow-[0_8px_32px_rgba(0,0,0,0.07)]"; + + // 行分隔:极淡,不带色 + const rowDivider = isDark ? "border-white/[0.04]" : "border-black/[0.04]"; + const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]"; - const codeBadge = isDark - ? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]" - : "bg-[#FFF1F0] text-[#CF1322]"; + const subText = isDark ? "text-slate-400" : "text-slate-500"; + + const filterTabs: { id: "all" | "configured" | "pending"; label: string }[] = [ + { id: "all", label: "全部业务" }, + { id: "configured", label: "已绑定" }, + { id: "pending", label: "待配置" }, + ]; return ( -
-
-
-

- 配置管理 -

-

- 为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。 -

-
+
+
- -
- - - - - - - - - - - {rows.length === 0 && !loading ? ( - - - - ) : ( - rows.map((row) => { - const editing = editingCode === row.businessCode; - return ( - - {/* 业务(只读) */} - + {/* ── 页头 banner ── */} + +
- {/* 智能体 */} -
- - {/* 业务链 */} - - - {/* 操作 */} - - - ); - }) +
+
+

+ 配置管理 +

+ -
业务智能体业务链操作
- 暂无业务数据 -
-
- - {row.label} - - - {row.businessCode} - -
-
- {editing ? ( - - {editing ? ( - - {editing ? ( -
- - -
- ) : ( - - )} -
+ > + + 智能会商神经中枢 + + + v1.2.5 + +
+ +

+ 业务流转与交叉验证的高级控制中心。在此注册高频业务模块并绑定专职执行智能体(Agent)及公共流转决策链(Chain),决策信息将基于该拓扑自动触发多智能体自诊断及会商组织。 +

+ +
+ + + 会商引擎: 正常运行中 + + • + + 全局配置项:{" "} + + {stats.total} 项注册 + + + • + + 指定专家节点:{" "} + + {stats.boundAgents} / {stats.total} + + +
- + + + {/* ── 表格区 ── */} + + {/* 搜索 + 筛选 */} +
+
+ + setSearchQuery(e.target.value)} + className={cn( + "w-full pl-10 pr-4 py-2.5 rounded-[5px] text-xs transition-all placeholder:text-slate-400 focus:outline-none", + isDark + ? "text-slate-200 bg-white/5 focus:bg-white/8 focus:ring-1 focus:ring-[#4D8DFF]/30" + : "text-slate-700 bg-slate-50/60 hover:bg-slate-50 focus:bg-white focus:ring-1 focus:ring-red-400/30", + )} + /> +
+ +
+ {filterTabs.map((tab) => ( + + ))} +
+
+ + {/* 表格 */} +
+ {loading ? ( +
加载中…
+ ) : ( + + + + + + + + + + + {filteredRows.length > 0 ? ( + filteredRows.map((row) => { + const editing = editingCode === row.businessCode; + return ( + + {/* 业务 */} + + + {/* 智能体 */} + + + {/* 业务链 */} + + + {/* 操作 */} + + + ); + }) + ) : ( + + + + )} + +
业务智能体业务链操作
+
+ + {row.label} + + + {row.businessCode} + +
+
+ {editing ? ( + + {editing ? ( + + {editing ? ( +
+ + +
+ ) : ( + + )} +
+
+ +

未找到符合筛选条件的业务配置

+ +
+
+ )} +
+ + {/* 表格尾栏 */} +
+ + + 会商程序会在对应业务的业务链生效时开启多体交叉核验证。 + + + 显示 {filteredRows.length} / {rows.length} 条 + +
+
+ + {/* ── 使用说明 ── */} + +
+ +
+
+

如何配置业务流转?

+

+ 1. 将鼠标悬停在任一业务行右侧,点击出现的{" "} + 「编辑」。 +
+ 2. 在该行内直接搜索并选择 执行智能体 与 业务链,点击「保存」即时生效。 +
+ 3. 若无需绑定,可搜索框清空后保存,置为「未配置」状态。 +

+
+
+
); diff --git a/frontend-web/src/pages/LightAppListPage.tsx b/frontend-web/src/pages/LightAppListPage.tsx index b6bc9d54..b5145b83 100644 --- a/frontend-web/src/pages/LightAppListPage.tsx +++ b/frontend-web/src/pages/LightAppListPage.tsx @@ -119,7 +119,7 @@ export default function LightAppListPage() {

应用列表

- + 导航工作台
@@ -129,7 +129,7 @@ export default function LightAppListPage() {
{/* 搜索 */} -
+
setSearch(e.target.value)} placeholder="键入应用名称、目标地址、或描述,实时过滤检索…" - className="w-full rounded-xl border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25" + className="w-full rounded-[5px] border border-zinc-200 bg-zinc-50/50 py-3 pl-10 pr-10 text-sm transition focus:border-[#CF1322]/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-[#CF1322]/20 dark:border-[#1A2F5A] dark:bg-[#0C1836] dark:text-[#C5CFE0] dark:placeholder-[#4A6491] dark:focus:border-[#1677FF]/60 dark:focus:bg-[#122347] dark:focus:ring-[#1677FF]/25" /> {search && (
-
+
setSearch(e.target.value)} placeholder="搜索应用名称 / 描述..." - className="w-full rounded-lg border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10" + className="w-full rounded-[5px] border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10" /> {search && (
{viewMode === "table" ? ( -
+
@@ -304,12 +317,12 @@ export default function LightAppManagementPage() {
{app.appType === "window_open" ? ( - + 新页跳转 ) : ( - + iframe 嵌套 @@ -329,7 +342,7 @@ export default function LightAppManagementPage() { {app.appType === "iframe" && app.mountLocation ? ( - + {mountLabel(app.mountLocation)} @@ -357,7 +370,7 @@ export default function LightAppManagementPage() { activeParamPopover === app.appId ? null : app.appId ) } - className="inline-flex cursor-pointer items-center space-x-1.5 rounded border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100" + className="inline-flex cursor-pointer items-center space-x-1.5 rounded-[5px] border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100" > {app.routeParams.length}个 {activeParamPopover === app.appId ? ( @@ -376,7 +389,7 @@ export default function LightAppManagementPage() { className="fixed inset-0 z-40" onClick={() => setActiveParamPopover(null)} /> -
+
注入的路由参数 @@ -392,7 +405,7 @@ export default function LightAppManagementPage() { {app.routeParams.map((p, idx) => (
{p.key} @@ -432,14 +445,14 @@ export default function LightAppManagementPage() {
) : tableApps.length === 0 ? ( -
+
@@ -492,7 +505,7 @@ export default function LightAppManagementPage() { {tableApps.map((app) => (
{app.appType === "window_open" ? ( - + 新页跳转 ) : ( - + iframe 嵌套 @@ -536,14 +549,14 @@ export default function LightAppManagementPage() { {app.description || "暂无应用详细描述说明。"}

-
+
{app.url}
{viewMode === "table" && ( -
+

智能多维挂载规则:

@@ -627,10 +640,10 @@ export default function LightAppManagementPage() { onClick={() => setDialogOpen(false)} /> -
+
- +

@@ -639,7 +652,7 @@ export default function LightAppManagementPage() {

@@ -652,14 +665,13 @@ export default function LightAppManagementPage() { 应用类型 - + options={APP_TYPE_OPTIONS} + popupProps={DIALOG_SELECT_POPUP} + onChange={(v) => patchForm({ appType: v as LightAppType })} + />

{form.appType === "window_open" ? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。" @@ -677,7 +689,7 @@ export default function LightAppManagementPage() { value={form.name} onChange={(e) => patchForm({ name: e.target.value })} placeholder="如: 舆情看板" - className="h-9 rounded-[2px] border-[#DBDBDB] text-[14px]" + className="h-9 rounded-[5px] border-[#DBDBDB] text-[14px]" />

@@ -691,7 +703,7 @@ export default function LightAppManagementPage() { value={form.url} onChange={(e) => patchForm({ url: e.target.value })} placeholder="https://example.com/page 或 http://47.88.25.99:3000/" - className="h-9 rounded-[2px] border-[#DBDBDB] font-mono text-[14px]" + className="h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]" />
@@ -702,18 +714,14 @@ export default function LightAppManagementPage() { 挂载菜单 - + + options={PARAM_TYPE_OPTIONS} + popupProps={DIALOG_SELECT_POPUP} + onChange={(v) => patchParam(idx, { - paramType: e.target.value as "login" | "theme", - source: - e.target.value === "login" ? (p.source || "accessToken") : undefined, + paramType: v as "login" | "theme", + source: v === "login" ? (p.source || "accessToken") : undefined, }) } - className="h-8 w-full rounded-[2px] border border-[#DBDBDB] bg-[#FAFAFA] px-1 text-[12px] focus:border-[#F5222D] focus:outline-none" - > - - - + />
{p.paramType === "login" && (
- + options={LOGIN_SOURCE_OPTIONS} + popupProps={DIALOG_SELECT_POPUP} + onChange={(v) => patchParam(idx, { source: v as string })} + /> {p.source === "other" && ( )}
@@ -822,7 +826,7 @@ export default function LightAppManagementPage() { @@ -830,7 +834,7 @@ export default function LightAppManagementPage() { {/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */} {p.paramType === "theme" && ( -
+

各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)

@@ -858,7 +862,7 @@ export default function LightAppManagementPage() { }) } placeholder={c.key} - className="h-7 flex-1 rounded-[2px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" + className="h-7 flex-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" />
))} @@ -869,7 +873,7 @@ export default function LightAppManagementPage() { })}
) : ( -
+
暂无注入的路由参数,加载地址时不传递多余拼接。
)} @@ -898,7 +902,7 @@ export default function LightAppManagementPage() { variant="outline" disabled={saving} onClick={() => setDialogOpen(false)} - className="h-9 rounded-[2px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold" + className="h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold" > 取消 @@ -906,7 +910,7 @@ export default function LightAppManagementPage() { type="button" disabled={saving} onClick={handleSave} - className="h-9 cursor-pointer rounded-[2px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70" + className="h-9 cursor-pointer rounded-[5px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70" > {saving ? "保存中..." : editing ? "保存修改" : "立即注册"} diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index dbbdfd47..1d19c45b 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from 'react'; import { useTheme } from '@/contexts/ThemeContext'; import { loginByToken, loginByUsername } from '@/core/auth/api'; -import { setStoredAuth } from '@/core/auth'; +import { setStoredAuth, setStoredUserName } from '@/core/auth'; import { persistExternalLoginParams, readExternalLoginParams, @@ -114,6 +114,7 @@ export default function LoginPage() { setErrorMessage(''); try { let auth; + let loginUserName = ''; if (authToken) { // token 换登录:后端用 authToken 调上游接口换出用户名再登录 auth = await loginByToken(authToken); @@ -128,11 +129,14 @@ export default function LoginPage() { } auth = await loginByUsername(deerflowUsername, gatePassword); + loginUserName = deerflowUsername; } if (cancelled) { return; } setStoredAuth(auth); + // 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖) + setStoredUserName(loginUserName); persistSidebarEnabled(sidebarEnabled); setStatus('success'); router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath); diff --git a/frontend-web/src/pages/PageRoutes.tsx b/frontend-web/src/pages/PageRoutes.tsx index 1e9b0e43..6ae9372d 100644 --- a/frontend-web/src/pages/PageRoutes.tsx +++ b/frontend-web/src/pages/PageRoutes.tsx @@ -7,6 +7,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; import { useTheme } from "@/contexts/ThemeContext"; import { useLocation } from "react-router-dom"; +import { useLocalSettings } from "@/core/settings"; import WorkspaceRoutes from "./WorkspaceRoutes"; import StrategyRoutes from "./StrategyRoutes"; @@ -21,8 +22,15 @@ function PageLayout() { const location = useLocation(); const { theme } = useTheme(); const isDark = theme === "dark"; + const [settings] = useLocalSettings(); + // 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到 + // 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户 + // 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。 + const layoutMode = settings.display.layoutMode; + const chromeByPreference = + layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled; const pageSidebarAllowed = - sidebarEnabled && !isChatsIframeWorkspacePath(location.pathname); + chromeByPreference && !isChatsIframeWorkspacePath(location.pathname); // h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100% // 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套 diff --git a/frontend-web/src/pages/WorkspaceRoutes.tsx b/frontend-web/src/pages/WorkspaceRoutes.tsx index 44f64d54..31e27de3 100644 --- a/frontend-web/src/pages/WorkspaceRoutes.tsx +++ b/frontend-web/src/pages/WorkspaceRoutes.tsx @@ -108,6 +108,7 @@ export function WorkspaceLayout({ '--header-height': headerHidden ? '0rem' : '4rem', } as CSSProperties } + key={String(compactWorkspaceSidebar)} data-page-sidebar-collapsed={pageSidebarCollapsed} // 紧凑模式下历史侧栏默认收起(聊天区更宽),由聊天页右上角图标钮展开 defaultOpen={!compactWorkspaceSidebar} diff --git a/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx b/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx index 3f96a2cb..5a7faa6f 100644 --- a/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx +++ b/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx @@ -6,8 +6,8 @@ * - footer「配置」按钮 → 进入配置页(空白新建,等价于旧按钮的直接跳转)。 * * 纯导航 / 浏览用途,不参与研讨席位选择(席位选择走 `BusinessChainPicker`)。 - * 卡片席位名直接用落库快照(`seat.name`),无需实时校验,点「设置」进编辑页后 - * 会看到实时数据。 + * 卡片席位名优先用实时 agent 数据(`listAgents()`,与 `BusinessChainPicker` 一致), + * agent 改名后此弹窗也能同步显示;解析不到才回退到落库快照(`seat.name`)。 */ import { Fragment, useCallback, useEffect, useState } from "react"; import { @@ -30,6 +30,8 @@ import { } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; +import { listAgents } from "@/core/agents"; + import { listChains, setChainPublic, type ChainScope } from "../api/chains"; import { chainTagClasses, type BusinessChain } from "../lib/business-chain"; import { getAvatarTypeFor } from "../lib/roundtable-constants"; @@ -71,6 +73,27 @@ export function BusinessChainListBody({ // 筛选:我的 / 公共(已发布)。 const [scope, setScope] = useState("mine"); const [publishingId, setPublishingId] = useState(null); + // 实时名称表(id 小写 → 最新 name):用于把席位的落库快照名刷新成最新(agent 改名同步)。 + const [nameById, setNameById] = useState>(new Map()); + + useEffect(() => { + if (!active) return; + let cancelled = false; + void (async () => { + try { + const agents = await listAgents(); + if (cancelled) return; + const m = new Map(); + for (const a of agents) if (a?.id) m.set(a.id.toLowerCase(), a.name ?? a.id); + setNameById(m); + } catch { + // 静默失败:解析不到就回退到席位快照名。 + } + })(); + return () => { + cancelled = true; + }; + }, [active]); const refresh = useCallback(async (s: ChainScope) => { setLoading(true); @@ -171,6 +194,7 @@ export function BusinessChainListBody({ onConfigure(chain.id)} onTogglePublic={() => handleTogglePublic(chain)} @@ -218,11 +242,13 @@ export function BusinessChainListDialog({ function ChainRow({ chain, + nameById, publishing, onSettings, onTogglePublic, }: { chain: BusinessChain; + nameById: Map; publishing: boolean; onSettings: () => void; onTogglePublic: () => void; @@ -292,14 +318,20 @@ function ChainRow({ )} {/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。 */} - +
); } // ── 链条席位编排流(体现「同层并行 · 层间串行」)───────────────────────────── -function ChainSeatsFlow({ chain }: { chain: BusinessChain }) { +function ChainSeatsFlow({ + chain, + nameById, +}: { + chain: BusinessChain; + nameById: Map; +}) { // 名称回退表:用落库快照(seats)解析 stage 里的 agent_id。 const seatById = new Map(chain.seats.map((s) => [s.agent_id, s] as const)); @@ -322,6 +354,8 @@ function ChainSeatsFlow({ chain }: { chain: BusinessChain }) { const seatTag = (agentId: string) => { const seat = seatById.get(agentId); + // 优先实时名称(agent 改名同步)→ 落库快照 → agent_id。 + const label = nameById.get(agentId.toLowerCase()) ?? seat?.name ?? agentId; return ( - {seat?.name || agentId} + {label} ); }; diff --git a/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx b/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx index c9eb3fa3..9fce3033 100644 --- a/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx @@ -46,7 +46,13 @@ import { DASHBOARD_VIEW_STORAGE_PREFIX } from "@/roundtable-planning/pages/Dashb import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll"; import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics"; import type { UseStep3DashboardReturn } from "@/roundtable-planning/hooks/useStep3Dashboard"; -import { parseReportJson, stripReportFence } from "@/roundtable-planning/lib/report-template"; +import { + parseReportJson, + renderDashboardHtml, + stripReportFence, + type DashboardTheme, +} from "@/roundtable-planning/lib/report-template"; +import { useTheme } from "@/contexts/ThemeContext"; interface Step3DashboardModeProps { selectedModel: string; @@ -94,13 +100,32 @@ export function Step3DashboardMode({ openDashboardPreview, } = dashboard; - // 真实数据 → 大屏数据:优先本轮实时生成,其次草稿快照;都没有则 undefined(用 DEMO)。 - const dashboardData = useMemo(() => { - const report = dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null); - return report ? adaptReportData(report, formatNow()) : undefined; - }, [dashboard.data, savedReport?.dashboardJson]); + // 真实数据(report-json):优先本轮实时生成,其次草稿快照。应用渲染/网页渲染共用。 + const report = useMemo( + () => dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null), + [dashboard.data, savedReport?.dashboardJson], + ); - const hasResult = !!dashboardData || !!html; + // 应用渲染用的大屏数据(不传则 DashboardApp 回落 DEMO)。 + const dashboardData = useMemo( + () => (report ? adaptReportData(report, formatNow()) : undefined), + [report], + ); + + // 网页渲染主题跟随应用主题:renderDashboardHtml 是确定性的,主题切换时按 report + // 当场重渲(而非用 hook 在生成时烤死主题的 `html`),从而 iframe 大屏色随父元素变化。 + // 仅 light/dark 两套(lightblue 归 light,与页面 themeMode 映射一致)。 + const { theme } = useTheme(); + const dashThemeMode: DashboardTheme = theme === "dark" ? "dark" : "light"; + const dashboardHtml = useMemo( + () => + report + ? renderDashboardHtml(report, { generatedAt: formatNow(), theme: dashThemeMode }) + : html, + [report, dashThemeMode, html], + ); + + const hasResult = !!dashboardData || !!dashboardHtml; // 视图:有产物默认看「大屏页面」,否则进「交互区」看抽取过程。 const [view, setView] = useState(() => (hasResult ? "page" : "chat")); @@ -110,6 +135,27 @@ export function Step3DashboardMode({ // - "html" 网页渲染:直接把自包含 HTML 大屏塞进 iframe,与「新窗口打开 / 分享 / 下载」一致。 const [renderMode, setRenderMode] = useState("app"); + // 网页渲染:用 blob URL 作 iframe `src`(而非 `srcDoc`)。 + // 原因:`srcdoc` 文档的 base URL 继承自父页面,大屏内的纯 CSS `:target` 导航锚点 + // (#overview / #seat-x)会被解析成「父页面URL#seat-x」,导致点击导航时整个 iframe + // 跳去加载主应用并命中 HashRouter 的 404(Page not found)。blob: 文档有自己的 URL, + // 片段锚点在 iframe 内部解析,:target 切页正常 —— 与「新窗口打开」一致。 + // + // 必须在 **effect 内部**创建/回收 blob:StrictMode(dev) 会 setup→cleanup→setup 双触发 + // effect,若用 useMemo 跨两次 render 共享同一个 URL,cleanup 会把它 revoke 掉而 iframe + // 仍指向它 → 报「该文件可能已被移至别处」。effect 内每次 run 各自建 URL、cleanup 只回收 + // 本 run 的,最后提交的那个存活,故 StrictMode-safe。dashboardHtml 随主题变 → blob 重建。 + const [htmlBlobUrl, setHtmlBlobUrl] = useState(null); + useEffect(() => { + if (typeof window === "undefined" || !dashboardHtml) { + setHtmlBlobUrl(null); + return; + } + const url = URL.createObjectURL(new Blob([dashboardHtml], { type: "text/html;charset=utf-8" })); + setHtmlBlobUrl(url); + return () => URL.revokeObjectURL(url); + }, [dashboardHtml]); + // 绘制完成**不再**自动切到大屏页面:用户正在交互区看抽取过程/继续问答时,强行跳走很打断。 // 想看成品由用户手动点顶栏「查看大屏」。没有产物时初始即落交互区(见上 useState 初值)。 @@ -126,8 +172,8 @@ export function Step3DashboardMode({ const handleOpenNewWindow = () => { if (typeof window === "undefined") return; - if (renderMode === "html" && html) { - const blob = new Blob([html], { type: "text/html;charset=utf-8" }); + if (renderMode === "html" && dashboardHtml) { + const blob = new Blob([dashboardHtml], { type: "text/html;charset=utf-8" }); const blobUrl = URL.createObjectURL(blob); window.open(blobUrl, "_blank", "noopener,noreferrer"); // 延迟回收,给新标签页足够时间加载(过早 revoke 会打不开)。 @@ -195,9 +241,9 @@ export function Step3DashboardMode({