fix(knowledge): align cards and remove extra guidance
This commit is contained in:
1 parent
c8ce94b778
commit
2899009215
27 files changed
+516
-100
No files matched your search
@@ -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<HTMLButtonElement> {
|
||||
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<HTMLButtonElement, AccountMenuTriggerProps>(
|
||||
function AccountMenuTrigger({
|
||||
accountName, avatarUrl, collapsed = false, expandedWidth = 150, open = false,
|
||||
avatarTestId, surfaceTestId, className, style, type = 'button', ...props
|
||||
}, ref) {
|
||||
return <button {...props} ref={ref} type={type}
|
||||
className={cn('group motion-press flex h-[42px] min-h-[42px] min-w-0 p-0 text-left text-xs font-medium leading-5 text-[#26384d]', collapsed && 'w-full', className)}
|
||||
style={{ ...style, width: collapsed ? undefined : expandedWidth, maxWidth: '100%', flex: collapsed ? '0 1 auto' : `0 1 ${expandedWidth}px` }}>
|
||||
<span data-testid={surfaceTestId}
|
||||
className={cn('pointer-events-none flex h-full w-full min-w-0 items-center gap-2 rounded-[11px] border border-[#d9e3ee] bg-white px-2 py-1 text-xs font-medium leading-5 transition-colors group-hover:bg-[#eaf1f8]',
|
||||
open && 'bg-[#eaf1f8]',
|
||||
collapsed && 'justify-center px-0')}>
|
||||
<UserAvatar src={avatarUrl} initial={getAccountInitial(accountName)} testId={avatarTestId} className="h-7 w-7 shrink-0" />
|
||||
{!collapsed && <>
|
||||
<span className="min-w-0 flex-1 truncate">{accountName}</span>
|
||||
<ChevronRight aria-hidden="true" className={cn('h-3.5 w-3.5 shrink-0', open && 'rotate-90')} />
|
||||
</>}
|
||||
</span>
|
||||
</button>;
|
||||
},
|
||||
);
|
||||
@@ -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,26 +55,20 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return <div className="relative h-[3.125rem] w-32 max-w-full" data-testid="home-account-controls">
|
||||
<button ref={trigger} type="button" data-testid="home-account-menu-trigger"
|
||||
return <div className="relative h-[42px] max-w-full" style={{ width: expandedWidth }} data-testid="home-account-controls">
|
||||
<AccountMenuTrigger ref={trigger} data-testid="home-account-menu-trigger"
|
||||
accountName={accountName} avatarUrl={avatarUrl} expandedWidth={expandedWidth} open={open}
|
||||
surfaceTestId="home-account-trigger-surface"
|
||||
aria-label={`打开账户菜单:${accountName}`} aria-haspopup="menu" aria-expanded={open} aria-controls="home-account-menu"
|
||||
className="group motion-press relative h-full w-full text-left"
|
||||
onClick={() => setOpen(value => !value)}
|
||||
onKeyDown={event => {
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
||||
event.preventDefault(); setOpen(true); focusItem(event.key === 'ArrowUp');
|
||||
}
|
||||
}}>
|
||||
<span data-testid="home-account-trigger-surface"
|
||||
className="pointer-events-none absolute bottom-0 left-0 flex min-h-12 w-64 origin-bottom-left scale-50 items-center gap-3 rounded-2xl border border-border/80 bg-background px-4 py-2 text-sm font-medium shadow-sm group-hover:bg-surface-subtle">
|
||||
<UserAvatar src={avatarUrl} initial={initial} className="h-8 w-8 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">{accountName}</span>
|
||||
<ChevronUp aria-hidden="true" className={`h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</span>
|
||||
</button>
|
||||
}} />
|
||||
<DisclosureContent ref={menu} id="home-account-menu" open={open} role="menu" aria-label="账户菜单"
|
||||
data-testid="home-account-menu"
|
||||
className="glass-surface absolute bottom-[2.0625rem] left-0 z-50 w-72 max-w-[calc(100vw-2rem)] rounded-2xl border border-border/80 p-3 text-foreground shadow-float"
|
||||
className="glass-surface absolute bottom-[calc(100%+0.5rem)] left-0 z-50 w-72 max-w-[calc(100vw-2rem)] rounded-2xl border border-border/80 p-3 text-foreground shadow-float"
|
||||
innerClassName="max-h-[calc(100dvh-8rem)] overflow-y-auto"
|
||||
onKeyDown={event => {
|
||||
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
||||
@@ -87,7 +88,10 @@ export function HomeAccountMenu({ accountName, avatarUrl, onOpenProfile }: {
|
||||
<button type="button" role="menuitem" className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm hover:bg-surface-subtle"
|
||||
onClick={() => { setOpen(false); onOpenProfile(); }}><UserCircle2 className="h-4 w-4" />个人资料</button>
|
||||
<button type="button" role="menuitem" className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm hover:bg-surface-subtle"
|
||||
onClick={() => { setOpen(false); navigate('/settings'); }}><Settings className="h-4 w-4" />设置</button>
|
||||
onClick={() => {
|
||||
if (!canLeaveCurrentPage()) { toast.info('请先保存当前修改,再打开设置。'); return; }
|
||||
setOpen(false); navigate('/settings', { state: getSettingsReturnState(location) });
|
||||
}}><Settings className="h-4 w-4" />设置</button>
|
||||
<button type="button" role="menuitem" disabled={loggingOut} className="flex min-h-10 items-center gap-2 rounded-md px-1 text-left text-sm text-accent-strong hover:bg-accent-soft disabled:opacity-50"
|
||||
onClick={async () => {
|
||||
if (loggingOut) return;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { HomeAccountMenu } from './HomeAccountMenu';
|
||||
import { UserProfileDialog } from '@/components/profile/UserProfileDialog';
|
||||
import { useCurrentUserProfile } from '@/hooks/use-current-user-profile';
|
||||
import { getAuthUserDisplayName } from '@/lib/auth-user-display';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { getUserProfileDisplayName } from '@/stores/user-profile';
|
||||
|
||||
export function StandaloneAccountDock({ enforceProfileCompletion = true }: { enforceProfileCompletion?: boolean }) {
|
||||
const user = useAuthStore(state => state.user);
|
||||
const authenticated = useAuthStore(state => state.isAuthenticated());
|
||||
const { userProfile, profileRequired, profileSyncError, syncProfileNow } = useCurrentUserProfile(authenticated);
|
||||
const [profileOpen, setProfileOpen] = useState(false);
|
||||
const [dismissedError, setDismissedError] = useState<string | null>(null);
|
||||
if (!user || !authenticated) return null;
|
||||
const name = getUserProfileDisplayName(userProfile) || getAuthUserDisplayName(user) || user.username;
|
||||
return <>
|
||||
<div data-testid="standalone-account-dock" className="fixed bottom-3 left-2.5 z-40">
|
||||
<HomeAccountMenu key={user.userId ?? user.username} accountName={name} avatarUrl={userProfile?.avatarUrl}
|
||||
onOpenProfile={() => { setDismissedError(null); setProfileOpen(true); void syncProfileNow().catch(() => undefined); }} />
|
||||
</div>
|
||||
<UserProfileDialog open={profileOpen || (enforceProfileCompletion && (profileRequired || Boolean(profileSyncError && dismissedError !== profileSyncError)))}
|
||||
required={profileRequired} syncError={profileSyncError} onOpenChange={open => {
|
||||
setProfileOpen(open);
|
||||
if (open) setDismissedError(null);
|
||||
else if (profileSyncError) setDismissedError(profileSyncError);
|
||||
}} />
|
||||
</>;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { TitleBar } from './TitleBar';
|
||||
import { SettingsLayout } from './SettingsLayout';
|
||||
import { StandaloneAccountDock } from '@/components/account/StandaloneAccountDock';
|
||||
import { getAiModuleForPath } from '@/lib/ai-modules';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -74,6 +76,8 @@ export function MainLayout() {
|
||||
if (sidebarPeekCloseTimerRef.current) clearTimeout(sidebarPeekCloseTimerRef.current);
|
||||
}, []);
|
||||
|
||||
if (location.pathname === '/settings' || location.pathname.startsWith('/settings/')) return <SettingsLayout />;
|
||||
|
||||
return (
|
||||
<div data-testid="main-layout" data-classroom={isChatWorkspace} className="relative flex h-[100dvh] flex-col overflow-hidden bg-transparent font-sans">
|
||||
{/* Title bar: drag region on macOS, icon + controls on Windows */}
|
||||
@@ -103,11 +107,13 @@ export function MainLayout() {
|
||||
'relative min-h-0 min-w-0 flex-1 overflow-auto bg-background',
|
||||
(isPaintingModule || isCloudAgents || isChatWorkspace) ? 'basis-0 h-full overflow-hidden p-0' : 'p-5 sm:p-6',
|
||||
(isChatWorkspace || isCloudAgents) && !isPaintingModule && 'basis-0 overflow-hidden p-0',
|
||||
isCanvasWorkspace && 'pb-16 lg:pb-0',
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
{isCanvasWorkspace && <div className="lg:hidden"><StandaloneAccountDock enforceProfileCompletion={false} /></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { TitleBar } from './TitleBar';
|
||||
import { StandaloneAccountDock } from '@/components/account/StandaloneAccountDock';
|
||||
|
||||
export function SettingsLayout() {
|
||||
return <div data-testid="settings-layout" className="flex h-[100dvh] flex-col overflow-hidden bg-background text-foreground">
|
||||
<TitleBar integrated pageTitle="设置" showSidebarControls={false} />
|
||||
<div className="min-h-0 flex-1 overflow-auto pb-20"><Outlet /></div>
|
||||
<StandaloneAccountDock />
|
||||
</div>;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<div className="classroom-account-footer mt-auto shrink-0 px-2 pb-2 pt-1">
|
||||
<div className="classroom-account-footer mt-auto shrink-0 px-[10px] pb-3 pt-2.5">
|
||||
<div className="relative">
|
||||
<div className={cn(
|
||||
'flex items-center gap-2',
|
||||
sidebarCollapsed && 'flex-col-reverse',
|
||||
)}>
|
||||
<button
|
||||
ref={accountButtonRef}
|
||||
type="button"
|
||||
data-testid="sidebar-member-menu-trigger"
|
||||
aria-label={`打开账户菜单:${accountName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
onClick={() => setAccountMenuOpen((open) => !open)}
|
||||
className={cn(
|
||||
'motion-press flex min-h-9 min-w-0 flex-1 items-center gap-2 rounded-lg border-0 bg-transparent px-2 py-1 text-left text-sm font-medium text-foreground shadow-none transition-[background-color,color,transform] hover:bg-surface-subtle',
|
||||
accountMenuOpen && 'bg-background text-foreground',
|
||||
sidebarCollapsed && 'min-h-9 w-full justify-center px-0',
|
||||
)}
|
||||
>
|
||||
<UserAvatar
|
||||
src={accountAvatarUrl}
|
||||
initial={accountInitial}
|
||||
testId="sidebar-account-avatar"
|
||||
className="h-7 w-7 shrink-0"
|
||||
/>
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{accountName}</span>
|
||||
</span>
|
||||
<ChevronRight className={cn('h-3.5 w-3.5 shrink-0', accountMenuOpen && 'rotate-90')} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<SidebarUpdateButton collapsed={sidebarCollapsed} />
|
||||
<div className="flex w-full items-center">
|
||||
<AccountMenuTrigger
|
||||
ref={accountButtonRef}
|
||||
data-testid="sidebar-member-menu-trigger"
|
||||
accountName={accountName}
|
||||
avatarUrl={accountAvatarUrl}
|
||||
collapsed={sidebarCollapsed}
|
||||
expandedWidth={sidebarWidth - 21}
|
||||
open={accountMenuOpen}
|
||||
avatarTestId="sidebar-account-avatar"
|
||||
aria-label={`打开账户菜单:${accountName}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={accountMenuOpen}
|
||||
onClick={() => setAccountMenuOpen((open) => !open)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DisclosureContent
|
||||
|
||||
@@ -57,7 +57,7 @@ export function SidebarUpdateButton({ collapsed }: { collapsed: boolean }) {
|
||||
disabled={downloading}
|
||||
onClick={() => 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',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -120,10 +120,6 @@ export function CloudKnowledgePanel({ slug, onCreated, onDeleted, onCatalog, ref
|
||||
<Button type="button" variant="ghost" disabled={disabled || busy || catalogLoading} onClick={() => setRefresh(v => v + 1)}>刷新知识库与模型</Button>
|
||||
</div>
|
||||
{documentPending && selectedKnowledge && <p role="status" className="text-xs leading-5 text-amber-800">有未确认的文件操作,请先在当前知识库重试或确认状态,再切换知识库。</p>}
|
||||
<details className="agent-knowledge-help">
|
||||
<summary>使用说明与费用</summary>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">上传到知识库的文档可供获准使用此智能体的人检索。解析并建立索引后才会生效;embedding 向量化由平台承担,不计入用户用量、词元点数或智能体费用上限。知识处理如触发对话模型调用,仍按原模型实际用量计费。</p>
|
||||
</details>
|
||||
{!catalogLoading && !catalogError && !manageable.length && <p className="text-xs leading-5 text-muted-foreground">还没有可管理的知识库,可以新建一个保存自己的文件。MakeLore知识仅供检索。</p>}
|
||||
{selectedKnowledge && selectableIds !== undefined && !bindable(selectedKnowledge.kb_id) && <p role="status" className="text-xs leading-5 text-amber-800">
|
||||
{selectableIds === null ? '知识库选用状态待确认。' : '这个知识库当前暂不可选用。'}你仍可管理自己的文件,刷新目录后再试。
|
||||
|
||||
@@ -34,7 +34,7 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
|
||||
</select>
|
||||
<Button type="button" variant="ghost" size="sm" disabled={disabled || loading || owned.loading} onClick={onRefresh}>刷新知识库</Button>
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">{selectionFull ? '已选满100个知识库,请先移除一个再选择。' : '可多选,每次选择一个知识库加入配置。'}</p>
|
||||
{selectionFull && <p className="text-xs leading-5 text-muted-foreground">已选满100个知识库,请先移除一个再选择。</p>}
|
||||
{error ? <p role="alert" className="text-sm text-destructive">知识库目录读取失败,请刷新重试。</p>
|
||||
: !loading && ownedIds !== null && options.every(option => ownedIds.includes(option.key)) && <p className="text-xs leading-5 text-muted-foreground">暂无可选的MakeLore知识。平台开放给你读取的知识库会出现在这里。</p>}
|
||||
{owned.error && <p className="text-xs leading-5 text-muted-foreground">我的知识库目录暂不可用,仍可选用可读取的知识;上传文件请刷新重试。</p>}
|
||||
@@ -56,7 +56,6 @@ export function CloudKnowledgeSelector({ slug, options, selected, disabled, load
|
||||
</li>;
|
||||
})}
|
||||
</ul> : <p className="agent-resource-empty">还没有选择知识库</p>}
|
||||
<p className="text-xs leading-5 text-muted-foreground">选择后保存并应用配置,智能体即可使用这些知识。MakeLore知识只可检索,无法在这里修改。</p>
|
||||
</section>
|
||||
</div>;
|
||||
}
|
||||
@@ -43,16 +43,13 @@
|
||||
.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; 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-owned-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); align-items: stretch; 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-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; }
|
||||
@container (max-width: 620px) { .agent-knowledge-owned-workspace { grid-template-columns: minmax(0, 1fr); } }
|
||||
|
||||
@@ -116,7 +116,7 @@ export function ModuleSelection({ authRequired = true }: { authRequired?: boolea
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
{authenticated && authUser && <footer className="module-selection-account relative shrink-0 pb-2" aria-label="账户">
|
||||
{authenticated && authUser && <footer className="module-selection-account relative h-[58px] shrink-0" aria-label="账户">
|
||||
<HomeAccountMenu key={authUser.userId ?? authUser.username} accountName={userDisplayName || authUser.username}
|
||||
avatarUrl={userProfile?.avatarUrl} onOpenProfile={() => {
|
||||
setDismissedSyncError(null); setProfileDialogOpen(true);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* Makelore application configuration.
|
||||
*/
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useBlocker, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
@@ -40,10 +41,12 @@ import { cn } from '@/lib/utils';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { canLeaveCurrentPage, getSettingsReturnPath } from '@/lib/settings-navigation';
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const {
|
||||
setLanguage,
|
||||
launchAtStartup,
|
||||
@@ -146,6 +149,20 @@ export function Settings() {
|
||||
proxyServer,
|
||||
proxyServerDraft,
|
||||
]);
|
||||
const blocker = useBlocker(proxySettingsDirty || savingProxy);
|
||||
|
||||
useEffect(() => {
|
||||
if (blocker.state !== 'blocked') return;
|
||||
blocker.reset();
|
||||
toast.info('请先保存或还原当前修改,再离开设置。');
|
||||
}, [blocker]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!proxySettingsDirty && !savingProxy) return;
|
||||
const protectUnsavedSettings = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ''; };
|
||||
window.addEventListener('beforeunload', protectUnsavedSettings);
|
||||
return () => window.removeEventListener('beforeunload', protectUnsavedSettings);
|
||||
}, [proxySettingsDirty, savingProxy]);
|
||||
|
||||
const handleSaveProxySettings = async () => {
|
||||
setSavingProxy(true);
|
||||
@@ -237,9 +254,14 @@ export function Settings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main data-testid="settings-page" className="min-h-full bg-background">
|
||||
<main data-testid="settings-page" data-settings-dirty={proxySettingsDirty || savingProxy} className="min-h-full bg-background">
|
||||
<div className="mx-auto flex max-w-4xl flex-col gap-8 px-8 py-8">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button type="button" variant="ghost" aria-label="返回上一页" data-testid="settings-back-button"
|
||||
className="shrink-0 gap-2" onClick={() => {
|
||||
if (!canLeaveCurrentPage()) { toast.info('请先保存或还原当前修改,再返回。'); return; }
|
||||
navigate(getSettingsReturnPath(location.state));
|
||||
}}><ArrowLeft className="h-4 w-4" />返回</Button>
|
||||
<h1 className="text-2xl font-semibold tracking-normal">{t('title')}</h1>
|
||||
</div>
|
||||
|
||||
|
||||
+4
-10
@@ -431,16 +431,10 @@
|
||||
}
|
||||
|
||||
.module-selection-account [data-testid='home-account-controls'] {
|
||||
transform: translateY(1rem);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.module-selection-account [data-testid='home-account-controls'] {
|
||||
transform: translate(
|
||||
max(-1.5cm, calc(1rem - max(2rem, calc((100vw - 80rem) / 2)))),
|
||||
2rem
|
||||
);
|
||||
}
|
||||
position: fixed;
|
||||
bottom: 12px;
|
||||
left: 10px;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.module-option-card {
|
||||
|
||||
Reference in new issue
Block a user