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
|
|
@ -47,9 +47,10 @@ interface Props {
|
|||
apiBase: string;
|
||||
runtimeRevision?: number;
|
||||
onClose?: () => void;
|
||||
onWorldChanged?: () => void;
|
||||
}
|
||||
|
||||
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Props) {
|
||||
export default function AgentChat({ apiBase, runtimeRevision = 0, onClose, onWorldChanged }: Props) {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [conversations, setConversations] = useState<ConversationSummary[]>([]);
|
||||
const [activeConversationId, setActiveConversationId] = useState('');
|
||||
|
|
@ -172,6 +173,7 @@ export default function AgentChat({ apiBase, runtimeRevision = 0, onClose }: Pro
|
|||
setPendingActions(data.actions || []);
|
||||
setMessages(data.messages || messages);
|
||||
await refreshConversationList();
|
||||
if (decision === 'confirm') onWorldChanged?.();
|
||||
} catch (error) {
|
||||
setMessages(previous => [...previous, { role: 'assistant', content: `动作未执行:${error instanceof Error ? error.message : String(error)}`, timestamp: new Date().toISOString() }]);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import type { ChannelState, ModuleManifest } from '../api';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
registry: ModuleManifest[];
|
||||
channel: ChannelState | null;
|
||||
busy: boolean;
|
||||
message: string;
|
||||
onClose: () => void;
|
||||
onChange: (moduleId: string, installed: boolean, mounted: boolean) => void;
|
||||
onUndo: () => void;
|
||||
canUndo: boolean;
|
||||
}
|
||||
|
||||
export function ModuleLibrarySheet({ open, registry, channel, busy, message, onClose, onChange, onUndo, canUndo }: Props) {
|
||||
if (!open) return null;
|
||||
return (
|
||||
<div className="sheet-backdrop" role="presentation" onMouseDown={event => { if (event.target === event.currentTarget) onClose(); }}>
|
||||
<section className="module-library-sheet" role="dialog" aria-modal="true" aria-label="频道模块">
|
||||
<header>
|
||||
<div><small>个人初始化频道</small><h2>频道模块</h2><p>这里只显示灯塔已登记、当前版本真实可用的模块。</p></div>
|
||||
<button type="button" onClick={onClose} aria-label="关闭">×</button>
|
||||
</header>
|
||||
<div className="module-library-list">
|
||||
{registry.map(manifest => {
|
||||
const state = channel?.modules.find(module => module.id === manifest.id);
|
||||
const installed = state?.installed === true;
|
||||
const mounted = installed && state?.mounted === true;
|
||||
return (
|
||||
<article className="module-library-card" key={manifest.id}>
|
||||
<div className="module-library-icon" aria-hidden="true"><span /></div>
|
||||
<div className="module-library-copy">
|
||||
<div className="module-library-title"><h3>{manifest.name}</h3><span>{manifest.state === 'LIVE' ? '可用' : manifest.state}</span></div>
|
||||
<code>{manifest.id}</code>
|
||||
<p>{manifest.description}</p>
|
||||
<small>{manifest.dataPolicy}</small>
|
||||
</div>
|
||||
<div className="module-library-actions">
|
||||
{!installed && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}>安装到频道</button>}
|
||||
{installed && !mounted && <button className="primary-button" disabled={busy} onClick={() => onChange(manifest.id, true, true)}>打开</button>}
|
||||
{installed && mounted && <button className="secondary-button" disabled={busy} onClick={() => onChange(manifest.id, true, false)}>收起</button>}
|
||||
{installed && <button className="quiet-danger" disabled={busy} onClick={() => onChange(manifest.id, false, false)}>移除入口</button>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<footer>
|
||||
<span>{message || `频道状态修订 ${channel?.revision ?? 0}`}</span>
|
||||
{canUndo && <button type="button" onClick={onUndo} disabled={busy}>撤销上一步</button>}
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,11 +5,13 @@ type RouteId = 'fifth' | 'main' | 'sub' | 'zero' | 'zero-sense';
|
|||
interface Props {
|
||||
activeRoute: RouteId;
|
||||
knowledgeSelected: boolean;
|
||||
knowledgeInstalled: boolean;
|
||||
onRouteSelect: (route: RouteId) => void;
|
||||
onKnowledgeSelect: () => void;
|
||||
onEducationSelect: () => void;
|
||||
onSettingsOpen: () => void;
|
||||
onAccountOpen: () => void;
|
||||
onModuleLibraryOpen: () => void;
|
||||
channelTitle: string;
|
||||
channelSubtitle: string;
|
||||
}
|
||||
|
|
@ -48,11 +50,13 @@ function ModuleIcon({ kind }: { kind: 'knowledge' | 'education' }) {
|
|||
export function PlatformNavigation({
|
||||
activeRoute,
|
||||
knowledgeSelected,
|
||||
knowledgeInstalled,
|
||||
onRouteSelect,
|
||||
onKnowledgeSelect,
|
||||
onEducationSelect,
|
||||
onSettingsOpen,
|
||||
onAccountOpen,
|
||||
onModuleLibraryOpen,
|
||||
channelTitle,
|
||||
channelSubtitle,
|
||||
}: Props) {
|
||||
|
|
@ -111,11 +115,12 @@ export function PlatformNavigation({
|
|||
</nav>
|
||||
)}
|
||||
|
||||
<div className="module-heading"><span>已安装模块</span><button type="button" aria-label="添加模块">+</button></div>
|
||||
<div className="module-heading"><span>已安装模块</span><button type="button" aria-label="添加模块" onClick={onModuleLibraryOpen}>+</button></div>
|
||||
<nav className="platform-module-list" aria-label="已安装模块">
|
||||
<button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
|
||||
{knowledgeInstalled && <button type="button" className={knowledgeSelected ? 'selected' : ''} onClick={onKnowledgeSelect}>
|
||||
<ModuleIcon kind="knowledge" /><span>知识库</span>
|
||||
</button>
|
||||
</button>}
|
||||
{!knowledgeInstalled && <button type="button" className="module-empty-entry" onClick={onModuleLibraryOpen}><span>+</span><span>安装第一个模块</span></button>}
|
||||
</nav>
|
||||
|
||||
<div className="platform-nav-footer">
|
||||
|
|
|
|||
Loading…
Reference in a new issue