From 2cc542f1f88576e5e96886c171035f8870013ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=A8=E9=B1=BC=E8=BE=A3=E6=A4=92?= Date: Tue, 13 Oct 2026 15:15:27 +0800 Subject: [PATCH] fix: isolate settings and unify account controls with Code --- ...20261013-combined-trial-update-c42d19a7.md | 19 +- README.md | 4 +- src/components/account/AccountMenuTrigger.tsx | 38 ++ src/components/account/HomeAccountMenu.tsx | 32 +- .../account/StandaloneAccountDock.tsx | 29 ++ src/components/layout/MainLayout.tsx | 6 + src/components/layout/SettingsLayout.tsx | 11 + src/components/layout/Sidebar.tsx | 57 ++- src/components/layout/SidebarUpdateButton.tsx | 2 +- src/components/layout/TitleBar.tsx | 1 + src/lib/settings-navigation.ts | 30 ++ src/pages/Chat/classroom-workspace.css | 3 +- src/pages/CloudAgents/CloudKnowledge.tsx | 110 +++--- .../CloudKnowledgeAttachmentPicker.tsx | 280 -------------- .../CloudAgents/CloudKnowledgeDocuments.tsx | 14 +- .../CloudAgents/CloudKnowledgeSelector.tsx | 5 +- src/pages/CloudAgents/agent-config.css | 65 +--- src/pages/ModuleSelection/index.tsx | 2 +- src/pages/Settings/index.tsx | 28 +- src/styles/globals.css | 14 +- tests/e2e/cloud-agent-knowledge.spec.ts | 298 +++++---------- tests/e2e/main-navigation.spec.ts | 136 ++++++- tests/e2e/settings-proxy.spec.ts | 12 + tests/unit/account-menu-trigger.test.tsx | 108 ++++++ tests/unit/cloud-agents-page.test.tsx | 1 - tests/unit/cloud-knowledge-catalog.test.tsx | 10 +- tests/unit/cloud-knowledge-import.test.tsx | 345 ------------------ tests/unit/cloud-knowledge-picker.test.tsx | 102 ++++-- tests/unit/cloud-knowledge-status.test.tsx | 22 +- tests/unit/home-account-menu.test.tsx | 23 +- tests/unit/main-layout-module-gate.test.tsx | 13 + tests/unit/settings-navigation.test.ts | 30 ++ 32 files changed, 759 insertions(+), 1091 deletions(-) create mode 100644 src/components/account/AccountMenuTrigger.tsx create mode 100644 src/components/account/StandaloneAccountDock.tsx create mode 100644 src/components/layout/SettingsLayout.tsx create mode 100644 src/lib/settings-navigation.ts delete mode 100644 src/pages/CloudAgents/CloudKnowledgeAttachmentPicker.tsx create mode 100644 tests/unit/account-menu-trigger.test.tsx delete mode 100644 tests/unit/cloud-knowledge-import.test.tsx create mode 100644 tests/unit/settings-navigation.test.ts diff --git a/.project-docs/30-worklog/tasks/20261013-combined-trial-update-c42d19a7.md b/.project-docs/30-worklog/tasks/20261013-combined-trial-update-c42d19a7.md index c52efadd..be117efb 100644 --- a/.project-docs/30-worklog/tasks/20261013-combined-trial-update-c42d19a7.md +++ b/.project-docs/30-worklog/tasks/20261013-combined-trial-update-c42d19a7.md @@ -15,7 +15,22 @@ - Assemble and validate completed homepage and Agent changes, then update the existing local temporary application with both Main and Renderer while retaining its profile and login. - Keep the current temporary application as the target; no production backend deployment, repository push, primary checkout change or worktree cleanup. - Follow-up: restore access to the existing account controls on the standalone module homepage and update the current trial. -- Latest follow-up: restore the desktop title-left / three-card vertical layout with the Code card centered on the title; retain Robot hiding and account controls. +- Latest follow-up: independent Settings without Code workspace chrome, explicit return to the entry module, and shared Code account appearance/position across homepage and modules. Retain three vertical cards, title displacement, Robot hiding and latest deployed knowledge/recovery changes. + +## Independent Settings / Shared Code Account Follow-up · 2026-10-13 + +- Concurrent / Planning Gates: Passed in the same active task. Exact feature/planning owner, branch, worktree and base verified after interruption; current task/source reread, unchanged startup/domain/accepted-decision context retained. Read-only peer review found no Settings/MainLayout/account ownership or semantic conflict; recovery/knowledge/Canvas contracts and source records remain untouched. An active knowledge task may update the Renderer later, so rebind actual current metadata before deployment. +- User reports /settings inherits Code sidebar/collapse and has no back action; source confirms ProtectedLayout always renders MainLayout, whose getAiModuleForPath fallback returns programming for settings. Keep the existing authentication/setup guard, render settings through an independent shell without workspace sidebar/project context/collapse, and add explicit back navigation to a validated originating local route with full query/hash; direct access falls back to module homepage. +- Latest account instruction supersedes the earlier half-width/full-scale interpretations: all existing account triggers should match the pictured Code design and screen corner placement. Reuse one pure display trigger (Code default 150x42, 28px avatar/12px text/14px right chevron, white card/11px radius/current border), derive expanded width from the Code sidebar preference, keep collapsed avatar access, preserve wallet/profile/logout/menu lifecycles and account isolation. Standalone home/settings docks use Code's 10px left / 12px bottom inset; keep prior title right shift, equal card layout and Robot hiding. +- Plan: root owns local settings-return helper, Settings shell/back/leave protection, MainLayout branch, TitleBar native inset, standalone account dock, module footer positioning and README/task record. Display subtask owns shared trigger and Home/Sidebar adoption with narrowly scoped Code CSS; test subtask owns focused display/interaction verification. No Main/backend/settings-write contract or production deployment change. +- Verify local origin sanitization/back routing, no settings sidebar/collapse/project chrome, saved/unsaved leave behavior, native traffic-light safety, shared trigger geometry across homepage/settings/Code/Agents/Canvas and compact/collapsed accessibility. Run relevant units/type/lint/build and isolated Electron scenarios/screenshots. Stage final Renderer against fresh current source with byte-identical Main/resources/package, preserve live route/login/profile/focus and no real setting/account/purchase/chat changes. +- Current application remains a9fa221 over unchanged Main7c/PID9968/58003. Width-only 430233f was committed/verified but intentionally not deployed after this steering; no home-width snapshot or runtime mutation occurred. +- Source implemented: independent Settings shell/back with validated local full-origin routing, proxy dirty/saving application-navigation and beforeunload protection, Mac traffic-light inset, one shared account display trigger and standalone dock. Canvas compact layout now has the same account access and reserves its bottom space; duplicate automatic profile completion is suppressed only in that compact dock. Update button moved above the account row so it cannot squeeze its width. Existing auth/setup/profile/wallet/logout and updater contracts retained. +- Independent final review passed: Settings early return follows all hooks; ProtectedLayout stays; local origin sanitization rejects settings/self/external/unknown paths; dirty navigation cannot discard drafts; profile sync is deduplicated per account and only Sidebar enforces Canvas completion. Native inset leaves overlay/fullscreen behavior unchanged. +- Fresh runtime binding advanced to committed knowledge layout 2d4e44f4a023fc649d9064d4413284fbeb962d59. Read its task Scope/Intent/Promotion and verified no semantic conflict. Preserved its exact a9-to-2d delta across 11 source/test paths (including removed picker/tests), combined only the knowledge README paragraph, imported no foreign/canonical documents. Product difference against latest runtime remains this Settings/account scope. Old one-run input-clearing authorization from that peer is not inherited. +- Verification so far: 15 focused unit files / 154 tests passed across Settings/account/module/profile/wallet and retained knowledge flows; final assembled typecheck, scoped ESLint and complete build passed. All 8 dist-electron files, 57 resources and package bytes exactly match original running Main. Homepage four-width geometry and Settings proxy/admin/back/dirty fixture passed; knowledge five scenarios passed at 375/768/1024/1440 and desktop/mobile screenshots visually checked. Account cross-module scenario is undergoing fixture stabilization; no runtime activation yet. +- Account fixture observations: first comparison sampled a sidebar node replaced during lazy workspace mounting; now polls the same full exact geometry assertion. Repeated missing fake-account mount was traced to real Main did-finish-load auth-session push racing the fixture's mocked IPC/session storage. Fix confined to the isolated fake-session event boundary and a single reload; product authentication remains unchanged. Final deterministic result recorded below when complete. +- Final account Electron scenario passed once with retries disabled (5.8s), including full wallet/profile/logout fixture behavior, exact shared dimensions/styles across Code/Agents/Canvas/Home/Settings, origin-query restoration and 375px Canvas/Home/Settings. Its Code bootstrap returns real empty-array/catalog schemas so no fixture-induced page crash is mistaken for geometry failure. Root visually inspected desktop home/Code plus 375px Settings/Canvas screenshots; return, account menu and viewport placement are correct. Final test lint/typecheck pass; no further product rebuild required for fixture-only edits. All eight relevant UI scenarios passed across assembled product; historical unrelated Code inner-tab assertion remains outside this change. ## Homepage Position / Compact Account Follow-up · 2026-10-13 @@ -127,3 +142,5 @@ - Current-state / success criteria: the module homepage now exposes the same existing balance/recharge/profile/settings/logout functions as the module sidebars, with keyboard/close behavior, account isolation and persistent wallet lifecycle. Evidence: source 5f7c851, focused tests/fixture screenshots and verified live renderer overlay; no new billing/auth contract or contradictory decision. - Current-state / homepage layout: user explicitly replaces the desktop side-by-side cards with left title and right vertical image-left card stack. Code center aligns with h1 center; smaller windows place title above the stack. Evidence: source 383ce71, four-width geometry/screenshots and actual 1470px live proof. Account controls, Robot hiding and module permissions retained; no unresolved semantic conflict or further confirmation required. Promote only in a separate integration gate. + +- Current-state / global account and Settings: Settings has its own authenticated shell without module workspace/collapse/project chrome and explicit full-origin return; all existing account triggers share Code appearance and 10px-left/12px-bottom placement, with compact Canvas access. Evidence: latest user screenshots/instructions, isolated return/dirty/profile/wallet and geometry verification; no auth/billing/Main contract change. This supersedes only the intermediate half-width/half-scale account interpretation. No contradictory accepted direction or additional human confirmation required; promote only in Integration Gate. diff --git a/README.md b/README.md index e9d74e81..73d833bd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Code 会话保存的模型被下架后,会提示重新选择可用模型;用户可在原会话中切换,恢复时使用新选择并保留会话与历史消息。切换不会自动重发消息或自动选择替代模型。 -Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,首页展示 Agents、Code、Canvas 三个模块入口,Robot 入口隐藏;云智能体需配套配置 WS/Yuxi 服务。桌面采用左侧问候与标题、右侧三个横向插画卡片竖排的布局,Code 入口中心与“今天,想创作什么?”标题中心对齐,三个入口等高、间距均匀;标题组在桌面向右偏移约 1.5cm,小窗口将标题放在卡片上方;Agents 标语为“打造你想象中的AI助手”。首页左下角保留半宽的账户入口,头像、文字与按钮高度保持原尺寸,向左偏移约 1.5cm(小窗口保留边距)并靠近底部;展开菜单保持原尺寸,可查看本人词元点数与预占、打开词元点数与充值、个人资料、设置和退出登录;沿用侧栏的账号、钱包和资料规则。工作区左上角入口点击后返回模块入口页: +Makelore 是一个面向软件、视觉创作、智能机器人与个人云智能体的 AI 桌面工作台。当前版本为 `2.0.0`,首页展示 Agents、Code、Canvas 三个模块入口,Robot 入口隐藏;云智能体需配套配置 WS/Yuxi 服务。桌面采用左侧问候与标题、右侧三个横向插画卡片竖排的布局,Code 入口中心与“今天,想创作什么?”标题中心对齐,三个入口等高、间距均匀;标题组在桌面向右偏移约 1.5cm,小窗口将标题放在卡片上方;Agents 标语为“打造你想象中的AI助手”。各页左下角账户按钮统一沿用 Code 的白色圆角卡片、头像、字号和右箭头,展开时跟随 Code 侧栏宽度偏好,首页和设置页与 Code 保持相同的左侧与底部边距;展开菜单保持原尺寸,可查看本人词元点数与预占、打开词元点数与充值、个人资料、设置和退出登录;沿用侧栏的账号、钱包和资料规则。设置使用独立页面,不显示编程项目栏、项目标题或工作栏收起按钮;“返回”回到打开设置前的页面(保留查询参数),直接打开设置时回模块首页,未保存设置先保存或还原。工作区左上角入口点击后返回模块入口页: - `Makelore Code|AI 编程`:管理本地项目、项目智能体、对话、文件上下文、代码变更和运行时。 会话工作台采用浅蓝项目栏、彩色通用手柄图标与卡片选中态;中间对话/作品使用圆角标签,作品预览的后退、前进、刷新与网址输入统一放在标签右侧;只在作品页显示,窄窗口自动换行。右侧智能体咨询使用奶油色标题区。沿用真实项目和会话入口、侧栏伸缩与账户菜单,样式只作用于 Code 会话工作台。可编辑的空对话在输入区上方的可用区域中央显示童趣欢迎语;「去作品广场逛逛」入口紧贴输入框上方,两者一起位于底部。老师介绍以固定宽度浮在右上角,不占用中间布局;欢迎语和底部输入区始终相对项目栏右侧的整个内容区域居中;小窗口的欢迎内容独立滚动;发送首条消息后收起欢迎区。侧栏保留「作品广场」入口,只有点击后才在系统浏览器打开公共广场,不请求、摘取或内嵌展示广场作品。打开广场保留当前项目、对话、草稿和附件,不自动发送消息;老师求助按钮保留临时应用的最新文案「帮我整体看🧠」。 @@ -16,7 +16,7 @@ Makelore 是一个面向软件、视觉创作、智能机器人与个人云智 历史待确认记录收起在智能体列表标题区的「创建记录/待处理记录」入口,只有点击后才打开记录窗口;配置、聊天和创建页不再置顶这些旧提醒。记录保留智能体名称和原请求身份,用户可明确「再试一次」或「不再提醒」,不会自动重试或清除。当前创建失败继续在创建表单内提示;关闭记录窗口保留已收到的结果和密钥,切换配置、聊天或创建页后可返回列表继续查看。密钥须明确关闭结果后,才可处理其他记录。 - 高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域左右分区:左侧「上传自己的文件」管理本人知识库,右侧选择「MakeLore知识」和「我的知识库」,小窗口上下排列。左侧可新建库或追加资料,提供「从电脑上传」和「从聊天选择文件」;聊天文件选择面板显示目标知识库、可搜索的来源聊天和文件名称/类型/大小,支持多选,逐文件导入并保留独立操作身份,部分失败只重试失败项;也可明确确认放弃本页重试,已保存文件仍保留。仅选本人已上传的聊天附件,聊天正文和助手生成的文件不直接入库。上传和导入后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。使用说明与费用以折叠说明呈现。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。 + 高级配置的知识库支持下拉多选,分为「MakeLore知识」和「我的知识库」,已选项可单独移除。MakeLore知识来自当前账号有权读取的 Yuxi 共享目录,只显示名称与简介;移除仅取消当前智能体选用,不删除原库。知识库区域上方左右分区:左侧「上传自己的文件」仅放「新建知识库」入口及展开的创建表单,右侧「查看我的知识库」通过下拉框查看本人知识库、从电脑追加资料和管理文件;下方独立「选择知识库」,选择智能体使用的「MakeLore知识」和「我的知识库」。小窗口按新建、查看、选用顺序纵向排列。创建成功后在右侧打开新库;新建和查看均不会自动加入智能体配置,选用上限不影响管理与新建。仅提供「从电脑上传」作为添加文件入口,未选择管理目标时上传按钮置灰;聊天文件导入入口已移除。上传后自动解析、建立索引并持续显示真实处理状态;失败可重试且保留已有可用内容。使用说明与费用以折叠说明呈现。暂不可选用的个人库仍可管理文件,已有绑定保留并提示不可用;未确认请求在当前库保留同一操作身份供重试。知识库选择只改草稿,显式保存/应用后生效;个人云端执行已自动接入所选知识库的检索,无需另外勾选 knowledge-base Skill。此交互复用现有接口,无需新增后端补丁。 云知识库的 embedding 模型只由平台管理员在 Yuxi 后台维护:在「智能体管理 → 模型供应商」启用供应商,按供应商要求填写有效的服务地址和 API Key,添加并启用 embedding 模型并填写实际向量维度;embedding 向量化费用由平台承担,不计入用户用量、词元点数或智能体费用上限;one-api 不参与 embedding 配置,Works Square 只负责其他应计费模型调用的创建者词元点数账本。客户端区分目录加载中、加载失败与无可用模型,空状态提供管理员配置说明;「刷新知识库与模型」保留未提交名称,未确认的创建请求沿用原操作身份。 diff --git a/src/components/account/AccountMenuTrigger.tsx b/src/components/account/AccountMenuTrigger.tsx new file mode 100644 index 00000000..88ca7bd8 --- /dev/null +++ b/src/components/account/AccountMenuTrigger.tsx @@ -0,0 +1,38 @@ +import { forwardRef, type ButtonHTMLAttributes } from 'react'; +import { ChevronRight } from 'lucide-react'; +import { UserAvatar } from '@/components/profile/UserAvatar'; +import { getAccountInitial } from '@/components/profile/user-avatar-utils'; +import { cn } from '@/lib/utils'; + +export interface AccountMenuTriggerProps extends ButtonHTMLAttributes { + accountName: string; + avatarUrl?: string | null; + collapsed?: boolean; + expandedWidth?: number; + open?: boolean; + avatarTestId?: string; + surfaceTestId?: string; +} + +/** Shared Code account appearance; the containing menu owns behavior and width preferences. */ +export const AccountMenuTrigger = forwardRef( + function AccountMenuTrigger({ + accountName, avatarUrl, collapsed = false, expandedWidth = 150, open = false, + avatarTestId, surfaceTestId, className, style, type = 'button', ...props + }, ref) { + return ; + }, +); diff --git a/src/components/account/HomeAccountMenu.tsx b/src/components/account/HomeAccountMenu.tsx index be425314..e2507b33 100644 --- a/src/components/account/HomeAccountMenu.tsx +++ b/src/components/account/HomeAccountMenu.tsx @@ -1,11 +1,16 @@ import { useEffect, useRef, useState } from 'react'; -import { ChevronUp, LogOut, Settings, UserCircle2 } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; +import { LogOut, Settings, UserCircle2 } from 'lucide-react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; import { UserAvatar } from '@/components/profile/UserAvatar'; import { getAccountInitial } from '@/components/profile/user-avatar-utils'; import { DisclosureContent } from '@/components/ui/disclosure'; import { useAuthStore } from '@/stores/auth'; +import { useSettingsStore } from '@/stores/settings'; +import { normalizeClassroomSidebarWidth } from '@/lib/classroom-layout'; +import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation'; import { PointWallet } from './PointWallet'; +import { AccountMenuTrigger } from './AccountMenuTrigger'; export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: { accountName: string; @@ -14,7 +19,9 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: { }) { const user = useAuthStore(state => state.user); const logout = useAuthStore(state => state.logout); + const expandedWidth = useSettingsStore(state => normalizeClassroomSidebarWidth(state.classroomSidebarWidth) - 21); const navigate = useNavigate(); + const location = useLocation(); const [open, setOpen] = useState(false); const [loggingOut, setLoggingOut] = useState(false); const [error, setError] = useState(''); @@ -48,23 +55,17 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: { if (!user) return null; - return
- + }} /> { setOpen(false); onOpenProfile(); }}>个人资料 + onClick={() => { + if (!canLeaveCurrentPage()) { toast.info('请先保存当前修改,再打开设置。'); return; } + setOpen(false); navigate('/settings', { state: getSettingsReturnState(location) }); + }}>设置
); } diff --git a/src/components/layout/SettingsLayout.tsx b/src/components/layout/SettingsLayout.tsx new file mode 100644 index 00000000..16daa8e7 --- /dev/null +++ b/src/components/layout/SettingsLayout.tsx @@ -0,0 +1,11 @@ +import { Outlet } from 'react-router-dom'; +import { TitleBar } from './TitleBar'; +import { StandaloneAccountDock } from '@/components/account/StandaloneAccountDock'; + +export function SettingsLayout() { + return
+ +
+ +
; +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index dcd14316..a4cdc38c 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -36,6 +36,8 @@ import { subscribeCodingProjectOpenRequest, } from '@/lib/coding-project-entry'; import { PointWallet } from '@/components/account/PointWallet'; +import { AccountMenuTrigger } from '@/components/account/AccountMenuTrigger'; +import { canLeaveCurrentPage, getSettingsReturnState } from '@/lib/settings-navigation'; import { cn } from '@/lib/utils'; import { useCurrentUserProfile } from '@/hooks/use-current-user-profile'; import type { SidebarPeekSource } from './sidebar-peek'; @@ -361,8 +363,9 @@ export function Sidebar({ }; const navigateFromAccountMenu = (path: string) => { + if (path === '/settings' && !canLeaveCurrentPage()) { toast.info('请先保存当前修改,再打开设置。'); return; } setAccountMenuOpen(false); - navigate(path); + navigate(path, path === '/settings' ? { state: getSettingsReturnState(location) } : undefined); }; const openProfileDialog = () => { @@ -810,42 +813,26 @@ export function Sidebar({ onOpenChange={setProfileDialogOpen} /> -
+
-
- +
+
+ setAccountMenuOpen((open) => !open)} + /> +
void handleClick()} className={cn( - 'motion-press flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-accent/30 bg-accent text-primary-foreground shadow-soft hover:bg-accent-strong disabled:cursor-wait disabled:opacity-80', + 'motion-press flex h-10 w-10 max-w-full shrink-0 items-center justify-center rounded-full border border-accent/30 bg-accent text-primary-foreground shadow-soft hover:bg-accent-strong disabled:cursor-wait disabled:opacity-80', collapsed && 'shadow-none', )} > diff --git a/src/components/layout/TitleBar.tsx b/src/components/layout/TitleBar.tsx index 00a8ad64..043df9b1 100644 --- a/src/components/layout/TitleBar.tsx +++ b/src/components/layout/TitleBar.tsx @@ -212,6 +212,7 @@ function ProductTitleBar({ data-testid="titlebar-main-surface" className={cn( 'relative flex min-w-0 flex-1 bg-background', + nativeTrafficLights && !nativeFullscreen && !showSidebarControls && !overlay && 'pl-[88px]', overlay && 'pointer-events-none border-b-0 bg-transparent', nativeTrafficLights && 'drag-region', !overlay && (workspaceLayout || sidebarTitlebarHidden ? 'border-b-0' : 'border-b border-border/80'), diff --git a/src/lib/settings-navigation.ts b/src/lib/settings-navigation.ts new file mode 100644 index 00000000..cd54837b --- /dev/null +++ b/src/lib/settings-navigation.ts @@ -0,0 +1,30 @@ +import { AI_MODULE_SELECTION_PATH, getGuardedAiModuleForPath } from './ai-modules'; + +type SettingsLocation = { pathname: string; search?: string; hash?: string; state?: unknown }; + +function localReturnPath(value: unknown): string | null { + if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//') || value.includes('\\')) return null; + try { + const target = new URL(value, 'https://makelore.local'); + if (target.origin !== 'https://makelore.local') return null; + if (target.pathname !== AI_MODULE_SELECTION_PATH && !getGuardedAiModuleForPath(target.pathname)) return null; + return `${target.pathname}${target.search}${target.hash}`; + } catch { return null; } +} + +export function getSettingsReturnPath(state: unknown): string { + const candidate = state && typeof state === 'object' && 'settingsReturnTo' in state + ? (state as { settingsReturnTo?: unknown }).settingsReturnTo : null; + return localReturnPath(candidate) ?? AI_MODULE_SELECTION_PATH; +} + +export function getSettingsReturnState(location: SettingsLocation): { settingsReturnTo: string } { + const inSettings = location.pathname === '/settings' || location.pathname.startsWith('/settings/'); + return { settingsReturnTo: inSettings ? getSettingsReturnPath(location.state) + : localReturnPath(`${location.pathname}${location.search ?? ''}${location.hash ?? ''}`) ?? AI_MODULE_SELECTION_PATH }; +} + +/** Honor the current page's existing unsaved-input protection before internal navigation. */ +export function canLeaveCurrentPage(): boolean { + return typeof window === 'undefined' || window.dispatchEvent(new Event('beforeunload', { cancelable: true })); +} diff --git a/src/pages/Chat/classroom-workspace.css b/src/pages/Chat/classroom-workspace.css index ec6e0d00..a562e52c 100644 --- a/src/pages/Chat/classroom-workspace.css +++ b/src/pages/Chat/classroom-workspace.css @@ -36,8 +36,7 @@ [data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row[data-selected="true"] { background: #e0ecfa; border-color: #cedef0; color: #304f75; } [data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child { min-height: 35px; gap: 7px; } [data-testid="sidebar"][data-classroom="true"] .classroom-conversation-row > button:first-child > svg { color: #7891af; } -[data-testid="sidebar"][data-classroom="true"] .classroom-account-footer { padding: 10px 10px 12px; border-top: 1px solid #dde5ee; background: #edf3f9; } -[data-testid="sidebar"][data-classroom="true"] [data-testid="sidebar-member-menu-trigger"] { min-height: 42px; border: 1px solid #d9e3ee; border-radius: 11px; background: #fff; font-size: 12px; } +[data-testid="sidebar"][data-classroom="true"] .classroom-account-footer { border-top: 1px solid #dde5ee; background: #edf3f9; } .classroom-workspace [data-testid="coding-conversation-header"] { border-bottom-color: #e8edf3; background: #fff; color: #304968; } .classroom-tabs { display: flex; flex-shrink: 0; height: 46px; gap: 9px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; } .classroom-workspace-toolbar { display: flex; flex-wrap: wrap; flex-shrink: 0; align-items: center; gap: 6px 12px; min-height: 46px; padding: 6px 22px; border-bottom: 1px solid #e8edf3; } diff --git a/src/pages/CloudAgents/CloudKnowledge.tsx b/src/pages/CloudAgents/CloudKnowledge.tsx index a54ee14b..f3edb6cd 100644 --- a/src/pages/CloudAgents/CloudKnowledge.tsx +++ b/src/pages/CloudAgents/CloudKnowledge.tsx @@ -1,5 +1,5 @@ -import { useEffect, useId, useRef, useState } from 'react'; -import { Upload } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { Library, Upload } from 'lucide-react'; import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -10,11 +10,10 @@ import { CloudKnowledgeDocuments } from './CloudKnowledgeDocuments'; export interface CloudOwnedKnowledgeCatalog { databases: CloudKnowledge[] | null; loading: boolean; error: string } -export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, onCatalog, refreshToken = 0, disabled = false, selectionFull = false, selectedIds = [], selectableIds }: { +export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, refreshToken = 0, disabled = false, selectableIds }: { slug: string; onCreated: (kb: CloudKnowledge) => void; onDeleted?: (id: string) => void; onCatalog?: (value: CloudOwnedKnowledgeCatalog) => void; refreshToken?: number; disabled?: boolean; - selectionFull?: boolean; selectedIds?: string[]; - selectableIds?: string[] | null; onSelected?: (kb: CloudKnowledge) => void; + selectableIds?: string[] | null; }) { type CloudEmbeddingModel = CloudAgentOperations['knowledge']['output']['models'][number]; const [catalog, setCatalog] = useState<{ databases: CloudKnowledge[]; models: CloudEmbeddingModel[] } | null>(null); @@ -22,13 +21,13 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on const [name, setName] = useState(''); const [model, setModel] = useState(''); const [error, setError] = useState(''); + const [errorTarget, setErrorTarget] = useState<'creation' | 'management'>('management'); const [catalogError, setCatalogError] = useState(''); const [catalogLoading, setCatalogLoading] = useState(true); const [busy, setBusy] = useState(false); const [refresh, setRefresh] = useState(0); const [creating, setCreating] = useState(false); const [documentPending, setDocumentPending] = useState(false); - const targetId = useId(); const createIntent = useRef<{ operation_id: string; name: string; embedding_model: string } | null>(null); usePendingCloudInput(busy || documentPending || Boolean(createIntent.current) || (creating && Boolean(name.trim()))); const operationInFlight = useRef(false); @@ -51,10 +50,10 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on .finally(() => { if (current) setCatalogLoading(false); }); return () => { current = false; }; }, [slug, refresh, refreshToken]); - const act = async (operation: () => Promise) => { + const act = async (operation: () => Promise, target: 'creation' | 'management' = 'management') => { if (disabled || operationInFlight.current) return; operationInFlight.current = true; - setBusy(true); setError(''); + setBusy(true); setError(''); setErrorTarget(target); try { await operation(); } catch(e) { if (alive.current) setError(e instanceof Error ? e.message : '知识库操作失败'); } finally { operationInFlight.current = false; if (alive.current) setBusy(false); } @@ -64,30 +63,63 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on const selectedKnowledge = manageable.find(kb => kb.kb_id === kbId); const managementDisabled = disabled || busy || catalogLoading || Boolean(catalogError); const bindable = (id: string) => selectableIds === undefined || Boolean(selectableIds?.includes(id)); - return
-
-
-

选择或新建自己的知识库,再从电脑或聊天中添加资料。选用会加入右侧配置,保存并应用后生效。

- - {error &&

{error}

} + const creationControls =
+ + {creating &&
+ setName(e.target.value)} /> + + {!catalogLoading && !catalogError && catalog && !catalog.models.length &&
+

暂时没有可用的向量模型。Embedding 只在 Yuxi 配置,请联系平台管理员启用后刷新;向量化费用由平台承担,不计入用户用量或智能体费用上限。

+
+ 管理员配置说明 +
    +
  1. Yuxi 后台「智能体管理 → 模型供应商」:启用供应商,按供应商要求填写有效的服务地址和 API Key。
  2. +
  3. 在同一后台添加并启用 embedding 模型,填写该模型的实际向量维度。
  4. +
  5. 配置完成后回到这里点击「刷新知识库与模型」。
  6. +
+
+
} + +
} +
; + return
+
+
+ {creationControls} + {errorTarget === 'creation' && error &&

{error}

} +
+
+
+ {errorTarget === 'management' && error &&

{error}

} {catalogError &&

知识库与模型目录加载失败:{catalogError}。请刷新重试。

}
- -
+ {documentPending && selectedKnowledge &&

有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。

}
使用说明与费用

上传到知识库的文档可供获准使用此智能体的人检索。解析并建立索引后才会生效;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限。知识处理如触发对话模型调用,仍按原模型实际用量计费。

@@ -96,35 +128,6 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onSelected, on {selectedKnowledge && selectableIds !== undefined && !bindable(selectedKnowledge.kb_id) &&

{selectableIds === null ? '知识库选用状态待确认。' : '这个知识库当前暂不可选用。'}你仍可管理自己的文件,刷新目录后再试。

} - {creating &&
- setName(e.target.value)} /> - - {!catalogLoading && !catalogError && catalog && !catalog.models.length &&
-

暂时没有可用的向量模型。Embedding 只在 Yuxi 配置,请联系平台管理员启用后刷新;向量化费用由平台承担,不计入用户用量或智能体费用上限。

-
- 管理员配置说明 -
    -
  1. Yuxi 后台「智能体管理 → 模型供应商」:启用供应商,按供应商要求填写有效的服务地址和 API Key。
  2. -
  3. 在同一后台添加并启用 embedding 模型,填写该模型的实际向量维度。
  4. -
  5. 配置完成后回到这里点击「刷新知识库与模型」。
  6. -
-
-
} - -
} - {documentPending && selectedKnowledge &&

有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。

} {kbId && !selectedKnowledge && !catalogLoading && !catalogError &&

这个知识库已无法管理。{documentPending ? '未确认的操作仍保留在当前页面。' : '请重新选择自己的知识库。'}

{documentPending &&

放弃只清除本地重试请求,不会删除已保存的文件。重新进入后可确认服务器状态。

}
} - {kbId && -
; +
+
; } diff --git a/src/pages/CloudAgents/CloudKnowledgeAttachmentPicker.tsx b/src/pages/CloudAgents/CloudKnowledgeAttachmentPicker.tsx deleted file mode 100644 index 88c0509c..00000000 --- a/src/pages/CloudAgents/CloudKnowledgeAttachmentPicker.tsx +++ /dev/null @@ -1,280 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { CheckCircle2, FileText, Loader2, Search, X } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'; -import { cloudAgentsApi } from '@/lib/cloud-agents-api'; -import { usePendingCloudInput } from './CloudPending'; -import type { CloudAttachment, CloudKnowledgeFile, CloudThread } from '../../../shared/cloud-agents'; - -type Selection = { key: string; threadId: string; threadTitle: string; file: CloudAttachment }; -type ImportJob = Selection & { - operationId: string; status: 'queued' | 'running' | 'saved' | 'failed' | 'done'; - importedFile?: CloudKnowledgeFile; error?: string; -}; - -function fileDetails(file: CloudAttachment) { - const extension = file.file_name.split('.').pop()?.toLowerCase() ?? ''; - const type = file.file_name.includes('.') ? extension.toUpperCase() : '文件'; - const size = Number.isFinite(file.file_size) - ? file.file_size < 1024 ? `${file.file_size} B` : file.file_size < 1024 * 1024 - ? `${Math.ceil(file.file_size / 1024)} KB` : `${(file.file_size / (1024 * 1024)).toFixed(1)} MB` - : '大小未知'; - const reason = file.file_size > 5 * 1024 * 1024 ? '超过 5 MB,暂不能导入' - : !/^(txt|md|csv|json|docx|xlsx|pdf)$/.test(extension) ? '此格式暂不支持知识库导入' : ''; - return { type, size, reason }; -} - -function updatedDate(value: string) { - const date = new Date(value); - return Number.isFinite(date.getTime()) ? date.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric', year: 'numeric' }) : '更新时间未知'; -} -function errorMessage(error: unknown) { return error instanceof Error ? error.message : '请稍后重试。'; } - -export function CloudKnowledgeAttachmentPicker({ slug, kbId, kbName, busy, canRead = true, act, onImported, onPendingChange, onClose, onRefreshKnowledge }: { - slug: string; kbId: string; kbName: string; busy: boolean; canRead?: boolean; - act: (operation: () => Promise) => Promise; - onImported: (file: CloudKnowledgeFile) => Promise; - onPendingChange: (pending: boolean) => void; - onClose?: () => void; onRefreshKnowledge?: () => void; -}) { - const scope = slug + ':' + kbId; - const [threads, setThreads] = useState([]); - const [agentNames, setAgentNames] = useState>({}); - const [threadId, setThreadId] = useState(''); - const [threadQuery, setThreadQuery] = useState(''); - const [fileQuery, setFileQuery] = useState(''); - const [files, setFiles] = useState<{ threadId: string; items: CloudAttachment[] }>({ threadId: '', items: [] }); - const [threadsLoading, setThreadsLoading] = useState(true); - const [filesLoading, setFilesLoading] = useState(false); - const [threadError, setThreadError] = useState(''); - const [fileError, setFileError] = useState(''); - const [threadsRevision, setThreadsRevision] = useState(0); - const [filesRevision, setFilesRevision] = useState(0); - const [selected, setSelected] = useState([]); - const [jobs, setJobs] = useState([]); - const [running, setRunning] = useState(false); - const [completed, setCompleted] = useState(0); - const [abandoning, setAbandoning] = useState(false); - const jobsRef = useRef([]); - const runningRef = useRef(false); - const alive = useRef(true); - const activeScope = useRef(scope); - const permitted = useRef(canRead); - const callbacks = useRef({ onImported, onClose }); - useEffect(() => { callbacks.current = { onImported, onClose }; }, [onImported, onClose]); - useEffect(() => { permitted.current = canRead; }, [canRead]); - useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); - useEffect(() => { - activeScope.current = scope; - jobsRef.current = []; runningRef.current = false; - setJobs([]); setSelected([]); setThreadId(''); setThreadQuery(''); setFileQuery(''); setCompleted(0); setRunning(false); setAbandoning(false); - }, [scope]); - const unresolved = jobs.some(job => job.status !== 'done'); - const pending = running || unresolved || selected.length > 0; - const locked = busy || !canRead || running || unresolved; - usePendingCloudInput(pending); - useEffect(() => { onPendingChange(pending); return () => onPendingChange(false); }, [pending, onPendingChange]); - - useEffect(() => { - let live = true; - setThreads([]); setThreadError(''); setThreadsLoading(canRead); - if (!canRead) return; - const load = async () => { - try { - let page = await cloudAgentsApi.call('threads', {}); - const all = [...page.threads]; - const seen = new Set(); - while (live && page.next_offset !== null) { - if (seen.has(page.next_offset)) throw new Error('对话列表读取未完成,请重试。'); - seen.add(page.next_offset); - page = await cloudAgentsApi.call('threads', { offset: page.next_offset }); - all.push(...page.threads); - } - if (live) setThreads([...new Map(all.map(thread => [thread.thread_id, thread])).values()]); - } catch (error) { if (live) setThreadError(errorMessage(error)); } - finally { if (live) setThreadsLoading(false); } - }; - void load(); - return () => { live = false; }; - }, [scope, canRead, threadsRevision]); - - useEffect(() => { - let live = true; - setAgentNames({}); - if (!canRead) return; - const names: Record = {}; - const publish = () => { if (live) setAgentNames({ ...names }); }; - // Names are supplementary: an unavailable agent directory must not hide chat files. - const ownNames = async () => { - let page = await cloudAgentsApi.list(); - const seen = new Set(); - for (;;) { - for (const agent of page.agents) names[agent.slug] = agent.name; - publish(); - if (!live || !page.next_cursor || seen.has(page.next_cursor)) return; - seen.add(page.next_cursor); - page = await cloudAgentsApi.list(page.next_cursor); - } - }; - void ownNames().catch(() => undefined); - void cloudAgentsApi.call('received', {}).then(page => { - if (!live) return; - for (const agent of page.agents) names[agent.slug] = agent.name; - publish(); - }).catch(() => undefined); - return () => { live = false; }; - }, [scope, canRead, threadsRevision]); - - useEffect(() => { - let live = true; - setFiles({ threadId, items: [] }); setFileError(''); setFilesLoading(Boolean(threadId) && canRead); - if (!threadId || !canRead) return; - cloudAgentsApi.call('attachments', { thread_id: threadId }).then(page => { - if (live) setFiles({ threadId, items: page.attachments }); - }).catch(error => { if (live) setFileError(errorMessage(error)); }) - .finally(() => { if (live) setFilesLoading(false); }); - return () => { live = false; }; - }, [scope, canRead, threadId, filesRevision]); - - const currentThread = threads.find(thread => thread.thread_id === threadId); - const visibleThreads = threads.filter(thread => `${thread.title} ${agentNames[thread.agent_slug] ?? ''}`.toLocaleLowerCase().includes(threadQuery.trim().toLocaleLowerCase())); - const visibleFiles = files.threadId === threadId ? files.items.filter(file => file.file_name.toLocaleLowerCase().includes(fileQuery.trim().toLocaleLowerCase())) : []; - const retryJobs = jobs.filter(job => job.status !== 'done'); - const failureOnly = retryJobs.length > 0 && retryJobs.every(job => job.status === 'failed'); - const selectedKeys = new Set(selected.map(item => item.key)); - - function choose(file: CloudAttachment, checked: boolean) { - if (locked || !currentThread || filesLoading || fileError || fileDetails(file).reason) return; - const key = JSON.stringify([threadId, file.file_id]); - if (jobsRef.current.length && jobsRef.current.every(job => job.status === 'done')) { jobsRef.current = []; setJobs([]); } - setCompleted(0); - setSelected(previous => checked ? [...previous.filter(item => item.key !== key), { - key, threadId, threadTitle: currentThread.title || '未命名对话', file, - }] : previous.filter(item => item.key !== key)); - } - - async function importSelected() { - if (busy || !canRead || runningRef.current) return; - const attemptScope = scope; - const current = () => alive.current && activeScope.current === attemptScope; - const initialJobs: ImportJob[] = jobsRef.current.length ? jobsRef.current : selected.map(item => ({ - ...item, operationId: crypto.randomUUID(), status: 'queued' as const, - })); - const attempts = initialJobs.filter(job => job.status !== 'done'); - if (!attempts.length) return; - const update = (key: string, value: Partial) => { - jobsRef.current = jobsRef.current.map(job => job.key === key ? { ...job, ...value } : job); - setJobs([...jobsRef.current]); - }; - jobsRef.current = initialJobs; setJobs([...initialJobs]); - runningRef.current = true; setRunning(true); setAbandoning(false); - try { - await act(async () => { - for (const job of attempts) { - if (!current() || !permitted.current) return; - update(job.key, { status: 'running', error: '' }); - try { - const file = job.importedFile ?? await cloudAgentsApi.call('importKnowledgeAttachment', { - slug, kb_id: kbId, thread_id: job.threadId, attachment_id: job.file.file_id, operation_id: job.operationId, - }); - if (!current()) return; - // A confirmed import is never repeated, even if permission changes or processing fails. - update(job.key, { status: 'saved', importedFile: file }); - if (!permitted.current) return; - try { - await callbacks.current.onImported(file); - if (!current()) return; - update(job.key, { status: 'done', error: '' }); - } catch (error) { - if (!current()) return; - update(job.key, { status: 'done', error: '文件已导入,处理未全部启动。请在知识库重试处理。' + errorMessage(error) }); - } - } catch (error) { - if (!current()) return; - update(job.key, { status: 'failed', error: '导入结果尚未确认:' + errorMessage(error) }); - } - } - if (current() && jobsRef.current.every(job => job.status === 'done')) { - setSelected([]); setCompleted(jobsRef.current.length); - if (jobsRef.current.every(job => !job.error)) { - jobsRef.current = []; setJobs([]); - callbacks.current.onClose?.(); - } - } - }); - } finally { - if (current()) { runningRef.current = false; setRunning(false); } - } - } - - const close = () => { if (!runningRef.current && !unresolved) onClose?.(); }; - return { if (!open) close(); }}> - { if (runningRef.current || unresolved) event.preventDefault(); }} - onInteractOutside={event => { if (runningRef.current || unresolved) event.preventDefault(); }}> -
-
从聊天选择文件导入到「{kbName}」
- {onClose && } -
-
-

选择你在 MakeLore Agents 聊天中上传的文件。聊天文字和助手生成的文件不会导入。

- {!canRead &&

知识库管理权限待确认,文件操作已暂停;未完成请求仍保留。

} - {canRead &&
-
-

选择对话

-
-
- {threadsLoading ?

正在读取对话…

: threadError ?

对话读取失败:{threadError}

- : !threads.length ?

还没有个人智能体对话。

: !visibleThreads.length ?

没有匹配的对话。

: visibleThreads.map(thread => )} -
-
-
-

{currentThread?.title || '选择文件'}

-
-
- {!threadId ?

先选择一段对话,再挑选其中的文件。

: filesLoading ?

正在读取文件…

- : fileError ?

文件读取失败:{fileError}

- : !files.items.length ?

这段对话还没有上传文件。

: !visibleFiles.length ?

没有匹配的文件。

: visibleFiles.map(file => { - const details = fileDetails(file), key = JSON.stringify([threadId, file.file_id]); - return ; - })} -
-
-
} - {selected.length > 0 &&

已选 {selected.length} 个文件{locked ? '。' : ',可继续切换对话选择。'}

-
    {selected.map(item =>
  • {item.file.file_name}{item.threadTitle} -
  • )}
-
} - {jobs.length > 0 &&
    {jobs.map(job =>
  • - {job.file.file_name}{job.status === 'done' ? <>已导入,处理状态见知识库 : job.status === 'running' ? '正在导入…' : job.status === 'saved' ? '已保存,等待继续处理' : job.status === 'failed' ? '等待重试' : '等待导入'} - {job.error &&

    {job.error}

    } -
  • )}
} - {abandoning &&
-

这只会结束当前页面的重试。已保存的文件会保留,可刷新知识库确认。

-
-
} -
-
-

最大 5 MB;支持文本、Word、Excel、文本 PDF。导入后自动处理,可用于回答后才能检索。

- {onRefreshKnowledge && } - {unresolved && !running && !abandoning && } - -
- {completed > 0 &&

已导入 {completed} 个文件,处理状态见知识库。

} -
; -} diff --git a/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx b/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx index 1a778f84..0cd84b6d 100644 --- a/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx +++ b/src/pages/CloudAgents/CloudKnowledgeDocuments.tsx @@ -3,7 +3,6 @@ import { CheckCircle2, CircleAlert, FileText, Loader2 } from 'lucide-react'; import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import { Button } from '@/components/ui/button'; import { usePendingCloudInput } from './CloudPending'; -import { CloudKnowledgeAttachmentPicker } from './CloudKnowledgeAttachmentPicker'; import type { CloudKnowledgeFile } from '../../../shared/cloud-agents'; function documentState(file: CloudKnowledgeFile) { @@ -48,11 +47,11 @@ function documentState(file: CloudKnowledgeFile) { return { label: '状态待确认', detail: '请刷新状态后再操作。', stage: 0 }; } -export function CloudKnowledgeDocuments({ slug, kbId, kbName = '当前知识库', busy, act, onDeleteKnowledge, canRead = true, onPendingChange, onRefreshKnowledge }: { - slug: string; kbId: string; kbName?: string; busy: boolean; +export function CloudKnowledgeDocuments({ slug, kbId, busy, act, onDeleteKnowledge, canRead = true, onPendingChange }: { + slug: string; kbId: string; busy: boolean; act: (operation: () => Promise) => Promise; onDeleteKnowledge: () => void; - canRead?: boolean; onPendingChange?: (pending: boolean) => void; onRefreshKnowledge?: () => void; + canRead?: boolean; onPendingChange?: (pending: boolean) => void; }) { const [files, setFiles] = useState([]); const [pageCount, setPageCount] = useState(1); @@ -62,8 +61,6 @@ export function CloudKnowledgeDocuments({ slug, kbId, kbName = '当前知识库' const [readError, setReadError] = useState(''); const [uploading, setUploading] = useState(''); const [submitting, setSubmitting] = useState(null); - const [importing, setImporting] = useState(false); - const [importPending, setImportPending] = useState(false); const [deleting, setDeleting] = useState(null); const [processErrors, setProcessErrors] = useState>({}); const uploadIntents = useRef>({}); @@ -72,7 +69,7 @@ export function CloudKnowledgeDocuments({ slug, kbId, kbName = '当前知识库' const permitted = useRef(canRead); useEffect(() => { permitted.current = canRead; }, [canRead]); useEffect(() => { alive.current = true; return () => { alive.current = false; }; }, []); - const unresolved = Object.keys(uploadIntents.current).length > 0 || Object.keys(processIntents.current).length > 0 || importPending; + const unresolved = Object.keys(uploadIntents.current).length > 0 || Object.keys(processIntents.current).length > 0; usePendingCloudInput(unresolved); useEffect(() => { onPendingChange?.(unresolved); return () => onPendingChange?.(false); }, [onPendingChange, unresolved]); const refresh = () => setRevision(value => value + 1); @@ -156,13 +153,10 @@ export function CloudKnowledgeDocuments({ slug, kbId, kbName = '当前知识库' return
-

上传后自动解析并建立索引。最大 5 MB;支持 TXT、Markdown、CSV、JSON、Word、Excel、文本 PDF。

{uploading &&

{uploading}

} - {importing && setImporting(false)} onRefreshKnowledge={onRefreshKnowledge} />}
{files.length ? '已显示 ' + files.length + ' 份文档 · ' + states.filter(state => state.ready).length + ' 份可用' : '知识文档'} {active ? '处理中 · 状态自动更新' : '状态自动更新'} diff --git a/src/pages/CloudAgents/CloudKnowledgeSelector.tsx b/src/pages/CloudAgents/CloudKnowledgeSelector.tsx index 9888fb2e..0c78f04a 100644 --- a/src/pages/CloudAgents/CloudKnowledgeSelector.tsx +++ b/src/pages/CloudAgents/CloudKnowledgeSelector.tsx @@ -18,10 +18,9 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load const add = (id: string) => { if (!selectionFull && !selected.includes(id)) onChange([...selected, id]); }; return
option.key)} - onSelected={kb => { if (!loading && !error && options.some(option => option.key === kb.kb_id)) { add(kb.kb_id); onRefresh(); } }} + selectableIds={loading || error ? null : options.map(option => option.key)} onDeleted={id => { onChange(selected.filter(value => value !== id)); onRefresh(); }} - onCreated={kb => { add(kb.kb_id); onRefresh(); }} /> + onCreated={onRefresh} />

从 MakeLore知识和我的知识库中选择,可同时使用多个。

diff --git a/src/pages/CloudAgents/agent-config.css b/src/pages/CloudAgents/agent-config.css index 4995f0b9..151fa693 100644 --- a/src/pages/CloudAgents/agent-config.css +++ b/src/pages/CloudAgents/agent-config.css @@ -42,65 +42,20 @@ .agent-config-resource-manager { margin-top: 18px; } .agent-config-resource-manager > details { border-radius: 14px; background: #ffffffa8; } .agent-resource-card:has(.agent-knowledge-workspace) { container-type: inline-size; } -.agent-knowledge-workspace { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); align-items: start; gap: 18px; min-width: 0; } -.agent-knowledge-upload, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; } +.agent-knowledge-workspace { display: grid; gap: 18px; min-width: 0; } +.agent-knowledge-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: start; gap: 18px; min-width: 0; } +.agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { min-width: 0; padding: 18px; border: 1px solid #dce5e0; border-radius: 17px; background: #fffdfb; overflow-wrap: anywhere; } .agent-knowledge-upload { border-color: #dce5f3; } .agent-knowledge-panel-heading { display: flex; align-items: center; gap: 9px; color: #33485c; } .agent-knowledge-panel-heading h3 { font-size: 16px; font-weight: 650; line-height: 1.5; } .agent-knowledge-panel-heading svg { flex-shrink: 0; color: #607d72; } .agent-knowledge-upload .agent-knowledge-panel-heading svg { color: #4a6e9c; } -.agent-knowledge-field-label { display: block; font-size: 13px; font-weight: 600; color: #4c586b; } .agent-knowledge-help { border-top: 1px solid #e8e6e1; padding-top: 12px; } .agent-knowledge-help summary { min-height: 28px; width: fit-content; cursor: pointer; color: #606c7e; font-size: 12px; line-height: 1.7; } .agent-knowledge-help summary:focus-visible { outline: 2px solid #9a7bb5; outline-offset: 3px; border-radius: 4px; } .agent-knowledge-selection ul { max-height: 480px; overflow: auto; overscroll-behavior: contain; } .agent-knowledge-selection button { flex-shrink: 0; } -.agent-knowledge-import.agent-knowledge-import-modal { display: flex; flex-direction: column; width: calc(100vw - 32px); max-width: 860px; max-height: calc(100dvh - 32px); gap: 18px; border-radius: 22px; padding: 24px; background: #fffdf9; color: #273246; } -.agent-knowledge-import-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-shrink: 0; min-width: 0; } -.agent-knowledge-import-header > div { min-width: 0; overflow-wrap: anywhere; } -.agent-knowledge-import-header p { margin-top: 6px; line-height: 1.7; color: #59657a; } -.agent-knowledge-import-content { display: flex; flex-direction: column; gap: 16px; min-height: 0; min-width: 0; overflow: auto; overscroll-behavior: contain; } -.agent-knowledge-import-help { color: #606c7e; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; } -.agent-knowledge-import-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); gap: 16px; min-height: 0; min-width: 0; overflow: auto; } -.agent-knowledge-import-content > .agent-knowledge-import-body { flex-shrink: 0; } -.agent-knowledge-import-chats, .agent-knowledge-import-files { display: flex; flex-direction: column; min-width: 0; min-height: 0; gap: 10px; } -.agent-knowledge-import-chats h3, .agent-knowledge-import-files h3 { font-size: 14px; font-weight: 600; } -.agent-knowledge-import-search { display: flex; align-items: center; gap: 8px; min-width: 0; color: #606c7e; } -.agent-knowledge-import-search svg, .agent-knowledge-import-file > svg { flex-shrink: 0; } -.agent-knowledge-import :is(input:not([type="checkbox"]), select) { width: 100%; min-width: 0; min-height: 42px; border: 1px solid #cdd5e3; border-radius: 11px; background: #fff; padding: 10px 12px; font-size: 14px; } -.agent-knowledge-import :is(input, button):focus-visible { outline: 2px solid #9a7bb5; outline-offset: 2px; } -.agent-knowledge-import-chat-list, .agent-knowledge-import-file-list { min-width: 0; overflow: auto; max-height: 330px; overscroll-behavior: contain; border: 1px solid #dce2ec; border-radius: 13px; background: #fff; } -.agent-knowledge-import-chat-list > :is(p, div), .agent-knowledge-import-file-list > :is(p, div) { padding: 14px; color: #606c7e; font-size: 13px; line-height: 1.7; } -.agent-knowledge-import-chat { display: flex; flex-direction: column; gap: 5px; width: 100%; min-height: 68px; padding: 12px; text-align: left; font-size: 14px; line-height: 1.5; cursor: pointer; overflow-wrap: anywhere; transition: background-color 160ms ease; } -.agent-knowledge-import-chat + .agent-knowledge-import-chat { border-top: 1px solid #edf0f5; } -.agent-knowledge-import-chat:hover { background: #f6f5fa; } -.agent-knowledge-import-chat[aria-pressed="true"] { background: #f1f6ff; box-shadow: inset 3px 0 #6b87b3; } -.agent-knowledge-import-chat:disabled { cursor: not-allowed; } -.agent-knowledge-import-chat small { color: #606c7e; font-size: 12px; } -.agent-knowledge-import-file { display: flex; align-items: flex-start; gap: 10px; min-width: 0; min-height: 64px; padding: 12px; font-size: 14px; line-height: 1.6; cursor: pointer; overflow-wrap: anywhere; } -.agent-knowledge-import-file + .agent-knowledge-import-file { border-top: 1px solid #edf0f5; } -.agent-knowledge-import-file:has(input:checked) { background: #f6f2ff; } -.agent-knowledge-import-file:has(input:disabled) { cursor: not-allowed; } -.agent-knowledge-import-file input[type="checkbox"] { width: 17px; height: 17px; margin-top: 3px; accent-color: #79629d; flex-shrink: 0; } -.agent-knowledge-import-file > span { min-width: 0; } -.agent-knowledge-import-file small { display: block; font-size: 12px; color: #606c7e; } -.agent-knowledge-import-selection { flex-shrink: 0; min-width: 0; border-radius: 12px; background: #f3f0f8; padding: 12px; font-size: 13px; overflow-wrap: anywhere; } -.agent-knowledge-import-selection ul { max-height: 130px; overflow: auto; } -.agent-knowledge-import-selection li { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding-top: 8px; } -.agent-knowledge-import-selection li > span { min-width: 0; } -.agent-knowledge-import-selection li button { flex-shrink: 0; } -.agent-knowledge-import-selection small { display: block; color: #606c7e; font-size: 12px; } -.agent-knowledge-import-results { max-height: 170px; min-height: 0; flex-shrink: 0; overflow: auto; border: 1px solid #dce5e0; border-radius: 12px; padding: 12px; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; } -.agent-knowledge-import-results li + li { border-top: 1px solid #e8e6e1; margin-top: 10px; padding-top: 10px; } -.agent-knowledge-import-results li > span { display: flex; align-items: center; gap: 6px; } -.agent-knowledge-import-results li > span + span { color: #526c5f; } -.agent-knowledge-import-results [role="alert"] { color: #b54532; } -.agent-knowledge-import-abandon { flex-shrink: 0; padding: 12px; border: 1px solid #efd9ba; border-radius: 12px; background: #fff5e9; font-size: 13px; line-height: 1.7; } -.agent-knowledge-import-abandon p { margin-bottom: 10px; } -.agent-knowledge-import-abandon button { margin: 0 8px 6px 0; } -.agent-knowledge-import-footer { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: 9px; flex-shrink: 0; } -.agent-knowledge-import-footer p { flex: 1 1 280px; min-width: 0; font-size: 12px; line-height: 1.7; color: #606c7e; } -@container (max-width: 620px) { .agent-knowledge-workspace { grid-template-columns: minmax(0, 1fr); } } +@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } } @media (max-width: 640px) { .agent-editor-tabs { padding: 10px 12px; } .agent-editor-tabs [role="tab"] { gap: 6px; padding-inline: 8px; font-size: 14px; } @@ -112,12 +67,8 @@ .agent-config-heading-title { gap: 6px; } .agent-config-icon { width: 38px; height: 38px; border-radius: 13px; } .agent-config-section-top > button { margin-left: 48px; } - .agent-knowledge-workspace { grid-template-columns: minmax(0, 1fr); gap: 14px; } - .agent-knowledge-upload, .agent-knowledge-selection { padding: 14px; border-radius: 14px; } - .agent-knowledge-import.agent-knowledge-import-modal { padding: 18px; border-radius: 18px; gap: 14px; } - .agent-knowledge-import-body { grid-template-columns: minmax(0, 1fr); } - .agent-knowledge-import-chat-list { max-height: 160px; } - .agent-knowledge-import-file-list { max-height: 240px; } - .agent-knowledge-import-footer button { width: 100%; } + .agent-knowledge-workspace, .agent-knowledge-owned-workspace { gap: 14px; } + .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } + .agent-knowledge-upload, .agent-knowledge-management, .agent-knowledge-selection { padding: 14px; border-radius: 14px; } } -@media (prefers-reduced-motion: reduce) { .agent-resource-option, .agent-knowledge-import-chat { transition: none; } } +@media (prefers-reduced-motion: reduce) { .agent-resource-option { transition: none; } } diff --git a/src/pages/ModuleSelection/index.tsx b/src/pages/ModuleSelection/index.tsx index 9d39cd6e..1efc7209 100644 --- a/src/pages/ModuleSelection/index.tsx +++ b/src/pages/ModuleSelection/index.tsx @@ -116,7 +116,7 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea })}
- {authenticated && authUser &&