hololake-system-architecture/product-source/guanghu-knowledge-base/src/App.tsx

635 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
import { api, DocTreeNode, DocContent, type ChannelState, type ModuleManifest } from './api';
import { DocTree } from './components/DocTree';
import { Editor } from './components/Editor';
import { SearchBar } from './components/SearchBar';
import { VersionHistory } from './components/VersionHistory';
import AgentChat from './components/AgentChat';
import { PlatformNavigation } from './components/PlatformNavigation';
import { StorageLocationSheet } from './components/StorageLocationSheet';
import { HumanSettings, HumanPreferences, loadHumanPreferences } from './components/HumanSettings';
import { DomainSurface } from './components/DomainSurface';
import { ModuleLibrarySheet } from './components/ModuleLibrarySheet';
import { cleanDisplayText } from './presentation';
import { WorldEntry } from './components/WorldEntry';
import { DomainConnectionSheet } from './components/DomainConnectionSheet';
import type { DomainAccessProjection } from './domain-connection';
import { createDomainEntryTarget, type DomainEntryTarget, type DomainNodeType } from './domain-entry-state';
import type { DomainRouteId } from './public-domain-directory';
type View = 'editor' | 'history';
type RouteId = DomainRouteId;
type ModuleId = 'knowledge' | 'education';
interface RepositoryStatus {
branch: string;
head: string;
clean: boolean;
ahead: number;
behind: number;
remote: { name: string; url: string } | null;
}
interface ServerSession {
authenticated: boolean;
nodeId: string;
username?: string;
}
interface ServerProfile {
id: string;
name: string;
purpose: 'personal-fifth-domain' | 'enterprise-lighthouse';
channelTitle?: string;
channelSubtitle?: string;
}
function findFirstDocument(nodes: DocTreeNode[]): string | null {
for (const node of nodes) {
if (node.type === 'document') return node.path;
const child = findFirstDocument(node.children || []);
if (child) return child;
}
return null;
}
export default function App() {
const agentApiBase = window.location.protocol === 'file:' ? 'http://127.0.0.1:3890' : '';
const [tree, setTree] = useState<DocTreeNode[]>([]);
const [treeLoaded, setTreeLoaded] = useState(false);
const [currentDoc, setCurrentDoc] = useState<DocContent | null>(null);
const [currentPath, setCurrentPath] = useState('');
const [view, setView] = useState<View>('editor');
const [sidebarOpen, setSidebarOpen] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [agentPanelOpen, setAgentPanelOpen] = useState(false);
const [importing, setImporting] = useState(false);
const [importMessage, setImportMessage] = useState<string | null>(null);
const [emptyDismissed, setEmptyDismissed] = useState(false);
const [activeRoute, setActiveRoute] = useState<RouteId>('fifth');
const [activeModule, setActiveModule] = useState<ModuleId>('knowledge');
const [storageSheetOpen, setStorageSheetOpen] = useState(false);
const [domainConnectionOpen, setDomainConnectionOpen] = useState(false);
const [domainEntryTarget, setDomainEntryTarget] = useState<DomainEntryTarget | null>(null);
const [storageSheetInitialMode, setStorageSheetInitialMode] = useState<'local' | 'server' | undefined>();
const [serverSession, setServerSession] = useState<ServerSession>({ authenticated: false, nodeId: '' });
const [serverProfiles, setServerProfiles] = useState<ServerProfile[]>([]);
const [repositoryStatus, setRepositoryStatus] = useState<RepositoryStatus | null>(null);
const [humanSettingsOpen, setHumanSettingsOpen] = useState(false);
const [humanPreferences, setHumanPreferences] = useState<HumanPreferences>(() => loadHumanPreferences());
const [agentRevision, setAgentRevision] = useState(0);
const [treeWidth, setTreeWidth] = useState(() => Number(localStorage.getItem('hololake.layout.tree-width')) || 258);
const [agentWidth, setAgentWidth] = useState(() => Number(localStorage.getItem('hololake.layout.agent-width')) || 460);
const [channelState, setChannelState] = useState<ChannelState | null>(null);
const [moduleRegistry, setModuleRegistry] = useState<ModuleManifest[]>([]);
const [moduleLibraryOpen, setModuleLibraryOpen] = useState(false);
const [moduleBusy, setModuleBusy] = useState(false);
const [moduleMessage, setModuleMessage] = useState('');
const [lastChannelReceipt, setLastChannelReceipt] = useState('');
const [worldEntered, setWorldEntered] = useState(false);
const [domainAccess, setDomainAccess] = useState<DomainAccessProjection>({ blockers: [], runtimeReady: false, stage: 'checking' });
const domainAccessRequest = useRef(0);
const storageMode = repositoryStatus?.remote ? 'server' : 'local';
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
const openDoc = useCallback(async (docPath: string) => {
setLoading(true);
setError(null);
try {
const doc = await api.getDoc(docPath);
setCurrentDoc(doc);
setCurrentPath(docPath);
setView('editor');
setActiveRoute('fifth');
setActiveModule('knowledge');
} catch (err: any) {
setError(`加载文档失败: ${err.message}`);
} finally {
setLoading(false);
}
}, []);
const refreshTree = useCallback(async () => {
try {
const nextTree = await api.getTree();
setTree(nextTree);
setTreeLoaded(true);
} catch (err: any) {
setError(`加载文档树失败: ${err.message}`);
setTreeLoaded(true);
}
}, []);
const openWikiTarget = useCallback(async (rawTarget: string) => {
const target = rawTarget.trim().replace(/^\/+/, '');
const normalized = target.toLocaleLowerCase().replace(/\.md$/u, '');
const flatten = (nodes: DocTreeNode[]): DocTreeNode[] => nodes.flatMap(node => [node, ...(node.children ? flatten(node.children) : [])]);
const match = flatten(tree).find(node => {
if (node.type !== 'document') return false;
const path = node.path.toLocaleLowerCase().replace(/\.md$/u, '');
const name = node.name.toLocaleLowerCase().replace(/\.md$/u, '');
return path === normalized || name === normalized || path.endsWith(`/${normalized}`);
});
if (match) {
await openDoc(match.path);
return;
}
setError(`没有找到知识页面:${target}`);
}, [openDoc, tree]);
const refreshRepositoryStatus = useCallback(async () => {
try {
const response = await fetch(`${agentApiBase}/api/forgejo/status`);
const data = await response.json();
if (data.ok) setRepositoryStatus(data.status);
} catch {
setRepositoryStatus(null);
}
}, [agentApiBase]);
const refreshServerSession = useCallback(async () => {
const server = (window as any).hololake?.server;
if (!server?.session) return;
try {
const profiles = await server.list() as ServerProfile[];
setServerProfiles(profiles);
const personal = profiles.find(profile => profile.purpose === 'personal-fifth-domain');
if (personal) {
try { await server.connect(personal.id); } catch { /* 会话状态继续按真实结果显示 */ }
}
setServerSession(await server.session(personal?.id));
} catch {
setServerSession({ authenticated: false, nodeId: '' });
}
}, []);
const refreshDomainAccess = useCallback(async (domainId = 'DOM-FIFTH-0001', nodeType: DomainNodeType = 'local-terminal') => {
const request = ++domainAccessRequest.current;
const server = (window as any).hololake?.server;
if (!server?.domainAccess) {
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['desktop_runtime_required'], domainId, nodeType, runtimeReady: false, stage: 'login-required' });
return;
}
try {
const result = await server.domainAccess(domainId, nodeType);
if (request === domainAccessRequest.current) setDomainAccess(result);
} catch {
if (request === domainAccessRequest.current) setDomainAccess({ blockers: ['domain_access_probe_failed'], domainId, nodeType, runtimeReady: false, stage: 'login-required' });
}
}, []);
const openDomainConnection = useCallback((routeId: DomainRouteId | null) => {
const target = routeId ? createDomainEntryTarget(routeId) : null;
setDomainEntryTarget(target);
setDomainConnectionOpen(true);
if (target) {
setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, runtimeReady: false, stage: 'checking' });
void refreshDomainAccess(target.domain.stableDomainId, target.nodeType);
}
}, [refreshDomainAccess]);
const selectDomainNodeType = useCallback((nodeType: DomainNodeType) => {
if (!domainEntryTarget) return;
const target = createDomainEntryTarget(domainEntryTarget.domain.routeId, nodeType);
setDomainEntryTarget(target);
setDomainAccess({ blockers: [], domainId: target.domain.stableDomainId, nodeType, runtimeReady: false, stage: 'checking' });
void refreshDomainAccess(target.domain.stableDomainId, nodeType);
}, [domainEntryTarget, refreshDomainAccess]);
const refreshChannel = useCallback(async () => {
try {
const [channel, registry] = await Promise.all([api.getChannel(), api.getModules()]);
setChannelState(channel);
setModuleRegistry(registry);
} catch (err: any) {
setError(`频道状态读取失败: ${err.message}`);
}
}, []);
useEffect(() => {
refreshTree();
refreshRepositoryStatus();
refreshServerSession();
refreshDomainAccess();
refreshChannel();
}, [refreshTree, refreshRepositoryStatus, refreshServerSession, refreshDomainAccess, refreshChannel]);
const changeModuleState = useCallback(async (moduleId: string, installed: boolean, mounted: boolean) => {
setModuleBusy(true);
setModuleMessage('');
try {
const result = await api.patchChannel({ operation: 'set_module_state', moduleId, installed, mounted });
setChannelState(result.channel);
setLastChannelReceipt(result.receipt.id);
setModuleMessage(installed ? (mounted ? '模块已安装并打开,已保留可撤销检查点' : '模块已收起,数据保持不变') : '模块入口已移除,知识数据与历史没有删除');
if (!installed || !mounted) setAgentPanelOpen(false);
} catch (err: any) {
setModuleMessage(err.message);
} finally {
setModuleBusy(false);
}
}, []);
const undoModuleChange = useCallback(async () => {
if (!lastChannelReceipt) return;
setModuleBusy(true);
try {
const result = await api.undoChannel(lastChannelReceipt);
setChannelState(result.channel);
setLastChannelReceipt('');
setModuleMessage('已恢复上一步频道状态;用户数据始终保留');
} catch (err: any) {
setModuleMessage(err.message);
} finally {
setModuleBusy(false);
}
}, [lastChannelReceipt]);
useEffect(() => {
const resolvedLanguage = humanPreferences.language === 'system'
? navigator.language
: humanPreferences.language;
document.documentElement.lang = resolvedLanguage;
}, [humanPreferences.language]);
useEffect(() => {
localStorage.setItem('hololake.human-preferences.v1', JSON.stringify(humanPreferences));
}, [humanPreferences]);
useEffect(() => {
if (!treeLoaded || currentPath || loading) return;
const firstDocument = findFirstDocument(tree);
if (firstDocument) openDoc(firstDocument);
}, [tree, treeLoaded, currentPath, loading, openDoc]);
const saveDoc = useCallback(async (title: string, body: string) => {
if (!currentPath) return;
setLoading(true);
try {
const doc = await api.updateDoc(currentPath, title, body);
setCurrentDoc(doc);
await refreshTree();
} catch (err: any) {
setError(`保存失败: ${err.message}`);
} finally {
setLoading(false);
}
}, [currentPath, refreshTree]);
const createDoc = useCallback(async (parentPath: string) => {
const name = prompt('文档文件名(不含 .md');
if (!name) return;
const title = prompt('文档标题:') || name;
const docPath = parentPath ? `${parentPath}/${name}.md` : `${name}.md`;
try {
const doc = await api.createDoc(docPath, title, `# ${title}\n\n在这里开始写作...\n`);
setCurrentDoc(doc);
setCurrentPath(docPath);
setEmptyDismissed(false);
await refreshTree();
} catch (err: any) {
setError(`创建失败: ${err.message}`);
}
}, [refreshTree]);
const deleteDoc = useCallback(async () => {
if (!currentPath || !confirm(`确认删除 ${currentPath}`)) return;
try {
await api.deleteDoc(currentPath);
setCurrentDoc(null);
setCurrentPath('');
await refreshTree();
} catch (err: any) {
setError(`删除失败: ${err.message}`);
}
}, [currentPath, refreshTree]);
const importFolder = useCallback(async () => {
const knowledge = (window as any).hololake?.knowledge;
if (!knowledge?.importFolder) {
setError('本地文件夹导入只在 HoloLake 桌面 App 中提供');
return;
}
setImporting(true);
setError(null);
setImportMessage(null);
try {
const result = await knowledge.importFolder();
if (result.cancelled) {
setImportMessage('已取消导入,现有知识库没有变化');
return;
}
if (!result.imported) {
setImportMessage(`没有找到可导入的文档;已跳过 ${result.skipped || 0} 个文件`);
return;
}
await refreshTree();
if (result.firstDocument) await openDoc(result.firstDocument);
setEmptyDismissed(false);
setImportMessage([
`已导入 ${result.imported} 篇文档`,
result.assets ? `${result.assets} 个图片资源` : '',
result.skipped ? `跳过 ${result.skipped} 个暂不支持的文件` : '',
result.failed?.length ? `${result.failed.length} 个文件失败` : '',
].filter(Boolean).join(' · '));
} catch (err: any) {
setError(`导入失败: ${err.message}`);
} finally {
setImporting(false);
}
}, [openDoc, refreshTree]);
const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库');
const knowledgeState = channelState?.modules.find(module => module.id === 'HL-MOD-KNOWLEDGE-001');
const knowledgeInstalled = knowledgeState?.installed !== false;
const knowledgeMounted = knowledgeInstalled && knowledgeState?.mounted !== false;
const moduleCollapsed = knowledgeInstalled && !knowledgeMounted;
const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && knowledgeMounted;
const breadcrumb = useMemo(
() => currentPath ? currentPath.split('/').map(cleanDisplayText).join(' / ') : '知识库',
[currentPath],
);
const language = humanPreferences.language === 'system'
? (navigator.language.toLowerCase().startsWith('zh') ? 'zh-CN' : 'en')
: humanPreferences.language;
const fontFamily = humanPreferences.font === 'serif'
? 'ui-serif, "Songti SC", Georgia, serif'
: humanPreferences.font === 'accessible'
? 'Arial, "PingFang SC", sans-serif'
: 'Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
const personalServer = serverProfiles.find(profile => profile.purpose === 'personal-fifth-domain');
const channelTitle = personalServer?.channelTitle || '我的第五域';
const channelSubtitle = personalServer?.channelSubtitle || '当前频道';
const startResize = useCallback((kind: 'tree' | 'agent', event: ReactPointerEvent<HTMLButtonElement>) => {
event.preventDefault();
const startX = event.clientX;
const startWidth = kind === 'tree' ? treeWidth : agentWidth;
document.body.classList.add('is-resizing');
const move = (moveEvent: PointerEvent) => {
if (kind === 'tree') {
setTreeWidth(Math.max(210, Math.min(420, startWidth + moveEvent.clientX - startX)));
} else {
setAgentWidth(Math.max(360, Math.min(Math.max(360, window.innerWidth * 0.52), startWidth - moveEvent.clientX + startX)));
}
};
const stop = () => {
document.body.classList.remove('is-resizing');
window.removeEventListener('pointermove', move);
window.removeEventListener('pointerup', stop);
};
window.addEventListener('pointermove', move);
window.addEventListener('pointerup', stop, { once: true });
}, [treeWidth, agentWidth]);
useEffect(() => { localStorage.setItem('hololake.layout.tree-width', String(Math.round(treeWidth))); }, [treeWidth]);
useEffect(() => { localStorage.setItem('hololake.layout.agent-width', String(Math.round(agentWidth))); }, [agentWidth]);
return (
<>
{!worldEntered && (
<WorldEntry
access={domainAccess}
onEnterFifthRuntime={() => {
setActiveRoute('fifth');
setActiveModule('knowledge');
setWorldEntered(true);
}}
onEnterLocalWorkspace={() => {
setActiveRoute('fifth');
setActiveModule('knowledge');
setWorldEntered(true);
}}
onOpenConnection={openDomainConnection}
/>
)}
<div
className="hololake-shell"
data-language={language}
data-appearance={humanPreferences.appearance}
style={{ '--human-font': fontFamily, '--reading-size': `${humanPreferences.readingSize}px` } as CSSProperties}
>
<header className="platform-topbar">
<div className="topbar-spacer" />
<div className="topbar-channel">{channelTitle} · {channelSubtitle}</div>
<div className="topbar-search"><SearchBar onSelect={openDoc} /></div>
<button className="theme-quick-toggle" type="button" onClick={() => setHumanPreferences(current => ({ ...current, appearance: current.appearance === 'mist-light' ? 'lake-night' : 'mist-light' }))}>
{humanPreferences.appearance === 'mist-light' ? '雾白' : '湖夜'}
</button>
<button className={`fifth-domain-session ${serverSession.authenticated ? 'authenticated' : ''}`} type="button" onClick={() => {
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}>
<span className="session-status-dot" aria-hidden="true" />
<span><strong>{serverSession.authenticated ? `代码频道 · ${serverSession.username}` : (personalServer ? '代码频道账号' : '配置我的服务器')}</strong><small>{personalServer?.id || '本机私有配置'}</small></span>
</button>
</header>
<div className="platform-body">
<PlatformNavigation
channelTitle={channelTitle}
channelSubtitle={channelSubtitle}
activeRoute={activeRoute}
knowledgeInstalled={knowledgeInstalled}
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
onRouteSelect={route => {
setActiveRoute(route);
setActiveModule('knowledge');
}}
onKnowledgeSelect={() => {
setActiveRoute('fifth');
setActiveModule('knowledge');
if (!knowledgeMounted) void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true);
}}
onEducationSelect={() => {
setActiveRoute('sub');
setActiveModule('education');
}}
onSettingsOpen={() => setHumanSettingsOpen(true)}
onAccountOpen={() => {
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}
onModuleLibraryOpen={() => setModuleLibraryOpen(true)}
/>
<section className="platform-workspace">
{!contentVisible ? (
moduleCollapsed && activeRoute === 'fifth' ? (
<div className="route-surface">
<h1></h1>
<p></p>
<button className="primary-button" onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true)}></button>
</div>
) : activeRoute === 'fifth' && !knowledgeInstalled ? (
<div className="route-surface">
<h1></h1>
<p>使</p>
<button className="primary-button" onClick={() => setModuleLibraryOpen(true)}></button>
</div>
) : (
<DomainSurface
activeDomain={(activeRoute === 'fifth' ? 'sub' : activeRoute) as 'main' | 'sub' | 'zero' | 'zero-sense'}
educationSelected={activeRoute === 'sub' && activeModule === 'education'}
onOpenSettings={() => setHumanSettingsOpen(true)}
/>
)
) : (
<>
<div className="knowledge-tabbar">
<button className="sidebar-toggle" onClick={() => setSidebarOpen(!sidebarOpen)} aria-label={sidebarOpen ? '收起页面栏' : '展开页面栏'}>
<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M9 4v16" /></svg>
</button>
<div className="document-tab"><span className="document-tab-icon" />{currentTitle}<button aria-label="关闭当前页面">×</button></div>
<button className="new-tab-button" onClick={() => createDoc('')} aria-label="新建页面"></button>
</div>
<div className="knowledge-module-bar">
<button className="storage-state-button" onClick={() => {
setStorageSheetInitialMode(undefined);
setStorageSheetOpen(true);
}}>
<svg viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="5.5" rx="7.5" ry="3" /><path d="M4.5 5.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6M4.5 11.5v6c0 1.7 3.4 3 7.5 3s7.5-1.3 7.5-3v-6" /></svg>
<span>{storageLabel}</span>
<strong>{storageMode === 'local' ? '托管到我的服务器' : repositoryStatus?.remote?.url.split('/').slice(-2).join(' / ').replace(/\.git$/, '')}</strong>
</button>
<div className="knowledge-module-actions">
{currentDoc && <>
<button className={view === 'editor' ? 'selected' : ''} onClick={() => setView('editor')}></button>
<button className={view === 'history' ? 'selected' : ''} onClick={() => setView('history')}></button>
<button className="danger-action" onClick={deleteDoc} aria-label="删除当前页面"></button>
</>}
<button onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, false)}></button>
</div>
</div>
<div
className={`knowledge-workbench ${sidebarOpen ? 'has-tree' : ''} ${agentPanelOpen ? 'has-agent' : ''}`}
style={{ '--tree-width': `${treeWidth}px`, '--agent-width': `${agentWidth}px` } as CSSProperties}
>
{sidebarOpen && (
<aside className="knowledge-sidebar">
<div className="knowledge-sidebar-heading"><span></span><button onClick={() => createDoc('')} aria-label="新建页面"></button></div>
<div className="knowledge-sidebar-actions">
<button className="import-button" onClick={importFolder} disabled={importing}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16V4m0 0L7.5 8.5M12 4l4.5 4.5M5 14v5h14v-5" /></svg>
{importing ? '正在导入…' : '导入本地文件夹'}
</button>
</div>
{importMessage && <div className="import-message">{importMessage}<button onClick={() => setImportMessage(null)}>×</button></div>}
<DocTree nodes={tree} currentPath={currentPath} onSelect={openDoc} onCreate={createDoc} />
</aside>
)}
{sidebarOpen && <button type="button" className="workspace-resizer tree-resizer" onPointerDown={event => startResize('tree', event)} aria-label="调整知识树宽度" />}
<main className="knowledge-content">
{error && <div className="kb-error">{error}<button onClick={() => setError(null)}>×</button></div>}
{loading && <div className="kb-loading"></div>}
{!currentDoc && treeLoaded && !loading && !emptyDismissed && (
<div className="knowledge-empty">
<button className="empty-close" onClick={() => setEmptyDismissed(true)} aria-label="关闭导入引导">×</button>
<div className="route-orbit" aria-hidden="true"><span /></div>
<h2></h2>
<p></p>
<div className="empty-actions">
<button className="primary-button" onClick={importFolder} disabled={importing}>{importing ? '正在导入…' : '选择本地文件夹'}</button>
<button className="secondary-button" onClick={() => createDoc('')}></button>
</div>
<small> MarkdownTXTCSVJSON YAML</small>
</div>
)}
{!currentDoc && emptyDismissed && !loading && (
<div className="quiet-empty"><h2></h2><p>使</p><button className="secondary-button" onClick={() => setEmptyDismissed(false)}></button></div>
)}
{currentDoc && view === 'editor' && <Editor doc={currentDoc} onSave={saveDoc} onWikiSelect={openWikiTarget} />}
{currentDoc && view === 'history' && <VersionHistory docPath={currentPath} />}
{currentDoc && <div className="document-path">{breadcrumb}</div>}
</main>
{agentPanelOpen && <button type="button" className="workspace-resizer agent-resizer" onPointerDown={event => startResize('agent', event)} aria-label="调整 HoloLake 宽度" />}
{agentPanelOpen && (
<aside className="agent-drawer">
<div className="agent-drawer-scope"><span></span><strong> · {currentTitle}</strong></div>
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} onWorldChanged={refreshChannel} />
</aside>
)}
</div>
</>
)}
{contentVisible && (
<>
{!agentPanelOpen && <button className="agent-launcher" onClick={() => {
if (window.innerWidth < 1500) setSidebarOpen(false);
setAgentPanelOpen(true);
}}>
<span className="launcher-orbit" aria-hidden="true" />
HoloLake
</button>}
</>
)}
</section>
</div>
<StorageLocationSheet
open={storageSheetOpen}
apiBase={agentApiBase}
currentRemote={repositoryStatus?.remote?.url}
initialMode={storageSheetInitialMode}
onClose={() => setStorageSheetOpen(false)}
onApplied={() => {
refreshRepositoryStatus();
refreshServerSession();
refreshDomainAccess();
}}
/>
<DomainConnectionSheet
access={domainAccess}
target={domainEntryTarget}
open={domainConnectionOpen}
onClose={() => setDomainConnectionOpen(false)}
onSelectTarget={routeId => openDomainConnection(routeId)}
onSelectNodeType={selectDomainNodeType}
onEnterRuntime={() => {
if (!domainEntryTarget || domainEntryTarget.domain.routeId !== 'fifth' || !domainAccess.runtimeReady || domainAccess.domainId !== domainEntryTarget.domain.stableDomainId) return;
setActiveRoute('fifth');
setActiveModule('knowledge');
setWorldEntered(true);
setDomainConnectionOpen(false);
}}
onOpenCodeChannel={() => {
setDomainConnectionOpen(false);
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}
/>
<HumanSettings
open={humanSettingsOpen}
preferences={humanPreferences}
onClose={() => setHumanSettingsOpen(false)}
onChange={setHumanPreferences}
onAgentChanged={() => setAgentRevision(revision => revision + 1)}
onManageServer={() => {
setHumanSettingsOpen(false);
setStorageSheetInitialMode('server');
setStorageSheetOpen(true);
}}
/>
<ModuleLibrarySheet
open={moduleLibraryOpen}
registry={moduleRegistry}
channel={channelState}
busy={moduleBusy}
message={moduleMessage}
canUndo={Boolean(lastChannelReceipt)}
onClose={() => setModuleLibraryOpen(false)}
onChange={(moduleId, installed, mounted) => void changeModuleState(moduleId, installed, mounted)}
onUndo={() => void undoModuleChange()}
/>
</div>
</>
);
}