fix(hololake): ship dynamic persona routes and coherent workspace
This commit is contained in:
parent
5105ec5e32
commit
787ba2ad3f
21 changed files with 1111 additions and 326 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { api, DocTreeNode, DocContent } from './api';
|
||||
import { DocTree } from './components/DocTree';
|
||||
import { Editor } from './components/Editor';
|
||||
|
|
@ -80,6 +80,8 @@ export default function App() {
|
|||
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 storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
||||
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
||||
|
|
@ -113,6 +115,23 @@ export default function App() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
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`);
|
||||
|
|
@ -152,6 +171,10 @@ export default function App() {
|
|||
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);
|
||||
|
|
@ -254,6 +277,30 @@ export default function App() {
|
|||
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 (
|
||||
<div
|
||||
className="hololake-shell"
|
||||
|
|
@ -265,6 +312,9 @@ export default function App() {
|
|||
<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);
|
||||
|
|
@ -282,6 +332,7 @@ export default function App() {
|
|||
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
|
||||
onRouteSelect={route => {
|
||||
setActiveRoute(route);
|
||||
setActiveModule('knowledge');
|
||||
setModuleCollapsed(false);
|
||||
}}
|
||||
onKnowledgeSelect={() => {
|
||||
|
|
@ -312,7 +363,7 @@ export default function App() {
|
|||
) : (
|
||||
<DomainSurface
|
||||
activeDomain={(activeRoute === 'fifth' ? 'sub' : activeRoute) as 'main' | 'sub' | 'zero' | 'zero-sense'}
|
||||
educationSelected={activeModule === 'education'}
|
||||
educationSelected={activeRoute === 'sub' && activeModule === 'education'}
|
||||
onOpenSettings={() => setHumanSettingsOpen(true)}
|
||||
/>
|
||||
)
|
||||
|
|
@ -345,7 +396,10 @@ export default function App() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="knowledge-layout">
|
||||
<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>
|
||||
|
|
@ -360,6 +414,8 @@ export default function App() {
|
|||
</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>}
|
||||
|
|
@ -379,26 +435,31 @@ export default function App() {
|
|||
{!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} />}
|
||||
{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)} />
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{contentVisible && (
|
||||
<>
|
||||
{agentPanelOpen && (
|
||||
<aside className="agent-drawer">
|
||||
<div className="agent-drawer-scope"><span>当前作用范围</span><strong>当前页面 · {currentTitle}</strong></div>
|
||||
<AgentChat apiBase={agentApiBase} onDocSelect={openDoc} runtimeRevision={agentRevision} />
|
||||
</aside>
|
||||
)}
|
||||
<button className={`agent-launcher ${agentPanelOpen ? 'open' : ''}`} onClick={() => setAgentPanelOpen(!agentPanelOpen)}>
|
||||
{!agentPanelOpen && <button className="agent-launcher" onClick={() => {
|
||||
if (window.innerWidth < 1500) setSidebarOpen(false);
|
||||
setAgentPanelOpen(true);
|
||||
}}>
|
||||
<span className="launcher-orbit" aria-hidden="true" />
|
||||
{agentPanelOpen ? '收起 HoloLake' : '询问 HoloLake'}
|
||||
</button>
|
||||
询问 HoloLake
|
||||
</button>}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
|||
Loading…
Reference in a new issue