feat: add HoloLake personal channel module runtime
This commit is contained in:
parent
bb96d5eb89
commit
a106b23f9b
21 changed files with 786 additions and 31 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useState, type CSSProperties, type PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { api, DocTreeNode, DocContent } from './api';
|
||||
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';
|
||||
|
|
@ -9,6 +9,7 @@ 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';
|
||||
|
||||
type View = 'editor' | 'history';
|
||||
|
|
@ -63,7 +64,6 @@ export default function App() {
|
|||
const [currentPath, setCurrentPath] = useState('');
|
||||
const [view, setView] = useState<View>('editor');
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [moduleCollapsed, setModuleCollapsed] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [agentPanelOpen, setAgentPanelOpen] = useState(false);
|
||||
|
|
@ -82,6 +82,12 @@ export default function App() {
|
|||
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 storageMode = repositoryStatus?.remote ? 'server' : 'local';
|
||||
const storageLabel = storageMode === 'server' ? '服务器已托管' : '仅本机';
|
||||
|
|
@ -96,7 +102,6 @@ export default function App() {
|
|||
setView('editor');
|
||||
setActiveRoute('fifth');
|
||||
setActiveModule('knowledge');
|
||||
setModuleCollapsed(false);
|
||||
} catch (err: any) {
|
||||
setError(`加载文档失败: ${err.message}`);
|
||||
} finally {
|
||||
|
|
@ -158,11 +163,53 @@ export default function App() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
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();
|
||||
}, [refreshTree, refreshRepositoryStatus, refreshServerSession]);
|
||||
refreshChannel();
|
||||
}, [refreshTree, refreshRepositoryStatus, refreshServerSession, 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'
|
||||
|
|
@ -260,7 +307,11 @@ export default function App() {
|
|||
|
||||
const activeRouteCopy = routeCopy[activeRoute];
|
||||
const currentTitle = cleanDisplayText(currentDoc?.meta.title || '知识库');
|
||||
const contentVisible = activeRoute === 'fifth' && activeModule === 'knowledge' && !moduleCollapsed;
|
||||
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],
|
||||
|
|
@ -329,27 +380,27 @@ export default function App() {
|
|||
channelTitle={channelTitle}
|
||||
channelSubtitle={channelSubtitle}
|
||||
activeRoute={activeRoute}
|
||||
knowledgeInstalled={knowledgeInstalled}
|
||||
knowledgeSelected={activeRoute === 'fifth' && activeModule === 'knowledge'}
|
||||
onRouteSelect={route => {
|
||||
setActiveRoute(route);
|
||||
setActiveModule('knowledge');
|
||||
setModuleCollapsed(false);
|
||||
}}
|
||||
onKnowledgeSelect={() => {
|
||||
setActiveRoute('fifth');
|
||||
setActiveModule('knowledge');
|
||||
setModuleCollapsed(false);
|
||||
if (!knowledgeMounted) void changeModuleState('HL-MOD-KNOWLEDGE-001', true, true);
|
||||
}}
|
||||
onEducationSelect={() => {
|
||||
setActiveRoute('sub');
|
||||
setActiveModule('education');
|
||||
setModuleCollapsed(false);
|
||||
}}
|
||||
onSettingsOpen={() => setHumanSettingsOpen(true)}
|
||||
onAccountOpen={() => {
|
||||
setStorageSheetInitialMode('server');
|
||||
setStorageSheetOpen(true);
|
||||
}}
|
||||
onModuleLibraryOpen={() => setModuleLibraryOpen(true)}
|
||||
/>
|
||||
|
||||
<section className="platform-workspace">
|
||||
|
|
@ -358,7 +409,13 @@ export default function App() {
|
|||
<div className="route-surface">
|
||||
<h1>知识库已收起</h1>
|
||||
<p>模块仍安装在永恒湖心频道中,重新打开不会改变本地或服务器数据。</p>
|
||||
<button className="primary-button" onClick={() => setModuleCollapsed(false)}>重新打开知识库</button>
|
||||
<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
|
||||
|
|
@ -392,7 +449,7 @@ export default function App() {
|
|||
<button className={view === 'history' ? 'selected' : ''} onClick={() => setView('history')}>历史</button>
|
||||
<button className="danger-action" onClick={deleteDoc} aria-label="删除当前页面">删除</button>
|
||||
</>}
|
||||
<button onClick={() => setModuleCollapsed(true)}>收起模块</button>
|
||||
<button onClick={() => void changeModuleState('HL-MOD-KNOWLEDGE-001', true, false)}>收起模块</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -444,7 +501,7 @@ export default function App() {
|
|||
{agentPanelOpen && (
|
||||
<aside className="agent-drawer">
|
||||
<div className="agent-drawer-scope"><span>当前作用范围</span><strong>当前页面 · {currentTitle}</strong></div>
|
||||
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} />
|
||||
<AgentChat apiBase={agentApiBase} runtimeRevision={agentRevision} onClose={() => setAgentPanelOpen(false)} onWorldChanged={refreshChannel} />
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -488,6 +545,17 @@ export default function App() {
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue