feat: 零点原核频道v1+登录双门地基+五湖开场——zero_point裁决链/灯塔查号/code_repo_login/通知卡重设计/浅色主题联动(冰朔20260815夜谕)

This commit is contained in:
冰朔 2026-08-16 00:02:05 +08:00
commit 07fd4c85ef
16 changed files with 3082 additions and 88 deletions

View file

@ -1,13 +1,19 @@
import { StrictMode, useCallback, useEffect, useMemo, useState } from 'react'
import { StrictMode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { invoke } from '@tauri-apps/api/core'
import DOMPurify from 'dompurify'
import { marked } from 'marked'
import { splitFrontmatter, documentOutline, jumpToHeading, MarkdownDocument, markdownHtml } from './modules/knowledge-render'
const TAG_TINTS = ['tag-lavender', 'tag-sky', 'tag-mint', 'tag-amber', 'tag-rose', 'tag-slate']
function tagTint(tag: string) {
let hash = 0
for (const char of tag) hash = (hash * 31 + (char.codePointAt(0) || 0)) % 997
return TAG_TINTS[hash % TAG_TINTS.length]
}
import './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system'
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system' | 'parlor' | 'generic'
type KnowledgeSource = 'native' | 'legacy'
interface HomeStatus {
@ -21,6 +27,8 @@ interface HomeStatus {
mcpRole: string
}
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number }
interface LoginSession { username: string; host: string; signedInAtUnixMs: number }
interface LoginReceipt { username: string; email: string; host: string }
interface PersonalChannelEvent { sequence: number; eventId: string; kind: string; summary: string; occurredAtUnixMs: number; receiptId: string; receiptHash: string }
interface PersonalChannelSnapshot { state: string; identity?: PersonalChannelIdentity; recentEvents: PersonalChannelEvent[]; integrity: { state: string; eventCount: number; receiptCount: number } }
interface KnowledgeDocumentSummary {
@ -76,6 +84,15 @@ interface ReceiptProjection { events: ReceiptEvent[] }
interface DiscoveryTicketReceipt { laneId: string; clientInstanceId: string; discoveryTicket: string }
interface ReleaseCandidate { candidateId: string; confirmationToken: string; currentVersion: string; version: string; notes: string; features: string[]; fixes: string[]; dataMigrationRequired: boolean }
interface ReleaseCheckReceipt { candidate?: ReleaseCandidate }
interface ZeroPointSnapshot {
route: string
binding: string
userNumber: string
lastValidCheck: number
graceDeadline: number
protocol: { gracePeriodDays: number; lighthouseAnchorUrl: string; lighthouseResolveUrl: string; coreChannelSource: string; origin: string }
syncNote: string
}
const previewStatus: HomeStatus = { directLocalBrokerState: 'UNVERIFIED', directConnectionCount: 0, resumableSessionCount: 0, codeRepositoryMountCount: 0, pnccReceiptCount: 0, updateState: 'UNPROVISIONED_FAIL_CLOSED', releaseRecoveryState: 'NONE', mcpRole: 'DISCOVERY_RECOVERY_COMPATIBILITY_ONLY' }
const previewPersonal: PersonalChannelSnapshot = { state: 'UNAVAILABLE', recentEvents: [], integrity: { state: 'UNKNOWN', eventCount: 0, receiptCount: 0 } }
@ -84,7 +101,7 @@ const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], a
const themes: Array<{ id: ThemeId; name: string }> = [
{ id: 'night', name: '夜湖星光' }, { id: 'dawn', name: '晨湖曦光' }, { id: 'nebula', name: '星云紫夜' }, { id: 'candle', name: '烛畔暖湖' }, { id: 'clear', name: '清浅澄湖' },
]
const viewLabels: Record<ViewId, string> = { overview: '总览', knowledge: '知识工作台', code: '代码频道', receipts: '回执', system: '系统详情' }
const viewLabels: Record<ViewId, string> = { overview: '总览', knowledge: '知识工作台', code: '代码频道', receipts: '回执', system: '系统详情', parlor: '会客厅', generic: '通用AI' }
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
@ -94,8 +111,14 @@ function getOrCreateLocalId(key: string, prefix: string) {
return generated
}
function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | 'system') {
function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | 'system' | 'login') {
const value = String(error)
if (value.includes('LOGIN_CREDENTIALS_INVALID')) return '账号或密码未被仓库接受。'
if (value.includes('LOGIN_USERNAME_INVALID')) return '账号格式不合法(仅限字母、数字、连字符、下划线)。'
if (value.includes('LOGIN_NETWORK_FAILED')) return '未能连通光湖代码频道服务器。'
if (value.includes('LOGIN_KEYCHAIN_FAILED')) return '本机钥匙串写入失败,凭证未能安全保存。'
if (value.includes('LOGIN_SESSION_CORRUPT')) return '登录会话已损坏,请重新登录。'
if (value.includes('LOGIN_VERIFICATION_FAILED')) return '仓库验证未通过,请稍后再试。'
if (value.includes('HOST_NOT_TRUSTED')) return '该地址不属于已登记的光湖代码频道。'
if (value.includes('URL_INVALID')) return '频道地址无效,请粘贴完整的 HTTPS 克隆地址。'
if (value.includes('DESTINATION_EXISTS')) return '该代码频道已经存在,无需再次克隆。'
@ -103,13 +126,14 @@ function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' |
if (value.includes('NO_SUPPORTED_FILES')) return '文件夹中没有可导入的知识文件。'
if (value.includes('SAVE_CONFLICT')) return '页面已在别处更新,请重新打开后再编辑。'
if (value.includes('FILE_UNSUPPORTED')) return '该文件不是当前可阅读的文本格式。'
if (context === 'login') return '登录未完成。'
if (context === 'identity') return '个人空间未能建立。'
if (context === 'knowledge') return '知识操作未完成,原文件未被覆盖。'
if (context === 'code') return '代码频道操作未完成。'
return '系统操作未完成。'
}
function Icon({ name }: { name: ViewId | 'search' | 'import' | 'folder' | 'copy' | 'arrow' | 'chevron' | 'file' | 'edit' | 'save' | 'back' }) {
function Icon({ name }: { name: ViewId | 'search' | 'import' | 'folder' | 'copy' | 'arrow' | 'chevron' | 'file' | 'edit' | 'save' | 'back' | 'download' | 'trash' | 'chevronDown' | 'settings' | 'more' | 'plus' }) {
const paths: Record<string, React.ReactNode> = {
overview: <><rect x="4" y="4" width="6" height="6" rx="1.5"/><rect x="14" y="4" width="6" height="6" rx="1.5"/><rect x="4" y="14" width="6" height="6" rx="1.5"/><rect x="14" y="14" width="6" height="6" rx="1.5"/></>,
knowledge: <><path d="M5 4.5h10a3 3 0 0 1 3 3V20H8a3 3 0 0 1-3-3Z"/><path d="M8 4.5v12.8A2.7 2.7 0 0 1 10.7 20"/></>,
@ -118,6 +142,14 @@ function Icon({ name }: { name: ViewId | 'search' | 'import' | 'folder' | 'copy'
system: <><circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.5-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14.2 3h-4.1l-.3 2.7a7 7 0 0 0-2 1.2l-2.5-1-2 3.4 2.1 1.5a7 7 0 0 0 0 2.4l-2 1.5 2 3.4 2.4-1a7 7 0 0 0 2 1.2l.3 2.7h4.1l.3-2.7a7 7 0 0 0 2-1.2l2.5 1 2-3.4-2.1-1.5A7 7 0 0 0 19 12Z"/></>,
search: <><circle cx="11" cy="11" r="6"/><path d="m16 16 4 4"/></>,
import: <><path d="M12 4v11M8 11l4 4 4-4"/><path d="M5 19h14"/></>,
download: <><path d="M12 4v12M7 11l5 5 5-5"/><path d="M4 17v3h16v-3"/></>,
trash: <><path d="M4 7h16M10 4h4M7 7l1 13h8l1-13M10 11v5M14 11v5"/></>,
chevronDown: <path d="M6 9l6 6 6-6"/>,
settings: <><circle cx="12" cy="12" r="3"/><path d="M12 3v2.5M12 18.5V21M4.6 6.8l1.8 1.8M17.6 15.4l1.8 1.8M3 12h2.5M18.5 12H21M4.6 17.2l1.8-1.8M17.6 8.6l1.8-1.8"/></>,
more: <><circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/></>,
plus: <path d="M12 5v14M5 12h14"/>,
parlor: <><path d="M4 5.5h16v11H9l-5 3.5Z"/><path d="M8 9.5h8M8 12.5h5"/></>,
generic: <><path d="M13 3 5 13.5h5L11 21l8-10.5h-5Z"/></>,
folder: <path d="M3.5 7.5h6l2-2h9v13h-17Z"/>,
file: <><path d="M6 3.5h8l4 4v13H6Z"/><path d="M14 3.5v4h4"/></>,
copy: <><rect x="8" y="8" width="10" height="11" rx="2"/><path d="M15 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h2"/></>,
@ -130,45 +162,6 @@ function Icon({ name }: { name: ViewId | 'search' | 'import' | 'folder' | 'copy'
return <svg viewBox="0 0 24 24" aria-hidden="true">{paths[name]}</svg>
}
function escapeHtml(value: string) {
return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;')
}
function splitFrontmatter(body: string) {
const normalized = body.replace(/\r\n/g, '\n')
if (!normalized.startsWith('---\n')) return { content: normalized, metadata: {} as Record<string, string> }
const closing = normalized.indexOf('\n---\n', 4)
if (closing < 0) return { content: normalized, metadata: {} as Record<string, string> }
const metadata: Record<string, string> = {}
for (const line of normalized.slice(4, closing).split('\n')) {
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line)
if (match) metadata[match[1]] = match[2].replace(/^['"]|['"]$/g, '')
}
return { content: normalized.slice(closing + 5), metadata }
}
function markdownHtml(body: string) {
const withWikiLinks = body.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_whole, target: string, label?: string) =>
`<button class="wiki-link" type="button" data-wiki="${escapeHtml(target.trim())}">${escapeHtml((label || target).trim())}</button>`)
const html = marked.parse(withWikiLinks, { async: false, gfm: true, breaks: false }) as string
return DOMPurify.sanitize(html, { ADD_ATTR: ['data-wiki'], ADD_TAGS: ['button'] })
}
function documentOutline(body: string) {
return splitFrontmatter(body).content.split('\n').flatMap((line, index) => {
const match = /^(#{1,4})\s+(.+)$/.exec(line.trim())
return match ? [{ id: `outline-${index}`, level: match[1].length, title: match[2].replace(/[*_`]/g, '') }] : []
}).slice(0, 24)
}
function MarkdownDocument({ body, onWiki }: { body: string; onWiki?: (target: string) => void }) {
const parsed = useMemo(() => splitFrontmatter(body), [body])
const html = useMemo(() => markdownHtml(parsed.content), [parsed.content])
return <article className="markdown-document" onClick={(event) => {
const element = (event.target as HTMLElement).closest<HTMLElement>('[data-wiki]')
if (element?.dataset.wiki && onWiki) onWiki(element.dataset.wiki)
}} dangerouslySetInnerHTML={{ __html: html }} />
}
interface TreeNode { name: string; key: string; folders: Map<string, TreeNode>; documents: KnowledgeDocumentSummary[] }
function knowledgeTree(documents: KnowledgeDocumentSummary[]) {
@ -189,7 +182,7 @@ function knowledgeTree(documents: KnowledgeDocumentSummary[]) {
function countTree(node: TreeNode): number {
return node.documents.length + [...node.folders.values()].reduce((sum, child) => sum + countTree(child), 0)
}
function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen }: { node: TreeNode; depth: number; expanded: Set<string>; active?: string; onToggle: (key: string) => void; onOpen: (item: KnowledgeDocumentSummary) => void }) {
function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen, onRemoveFolder, folderMenuKey, onFolderMenuToggle }: { node: TreeNode; depth: number; expanded: Set<string>; active?: string; onToggle: (key: string) => void; onOpen: (item: KnowledgeDocumentSummary) => void; onRemoveFolder?: (folder: TreeNode) => void; folderMenuKey?: string | null; onFolderMenuToggle?: (key: string) => void }) {
const folders = [...node.folders.values()].sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'))
const documents = [...node.documents].sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'))
return <>
@ -199,7 +192,8 @@ function KnowledgeTree({ node, depth, expanded, active, onToggle, onOpen }: { no
<button className="tree-folder" style={{ paddingLeft: 12 + depth * 14 }} type="button" onClick={() => onToggle(folder.key)}>
<span className={open ? 'tree-chevron open' : 'tree-chevron'}><Icon name="chevron"/></span><Icon name="folder"/><span>{folder.name}</span><em>{countTree(folder)}</em>
</button>
{open && <KnowledgeTree node={folder} depth={depth + 1} expanded={expanded} active={active} onToggle={onToggle} onOpen={onOpen}/>}
{onRemoveFolder && onFolderMenuToggle && <span className="tree-folder-tools"><button className="tree-folder-menu-button" type="button" title="文件夹操作" onClick={(event) => { event.stopPropagation(); onFolderMenuToggle(folder.key) }}><Icon name="more"/></button>{folderMenuKey === folder.key && <span className="tree-folder-menu"><button type="button" onClick={(event) => { event.stopPropagation(); onFolderMenuToggle(folder.key); onRemoveFolder(folder) }}><Icon name="trash"/></button></span>}</span>}
{open && <KnowledgeTree node={folder} depth={depth + 1} expanded={expanded} active={active} onToggle={onToggle} onOpen={onOpen} onRemoveFolder={onRemoveFolder} folderMenuKey={folderMenuKey} onFolderMenuToggle={onFolderMenuToggle}/>}
</div>
})}
{documents.map((document) => <button key={`${document.source}:${document.path}`} className={active === `${document.source}:${document.path}` ? 'tree-document active' : 'tree-document'} style={{ paddingLeft: 31 + depth * 14 }} type="button" onClick={() => onOpen(document)}>
@ -231,6 +225,25 @@ function HoloLakeApp() {
const [codeTree, setCodeTree] = useState<CodeTreeSnapshot | null>(null)
const [activeCodeFile, setActiveCodeFile] = useState<CodeFileProjection | null>(null)
const [codeMode, setCodeMode] = useState<'human' | 'source'>('human')
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.localStorage.getItem('hololake-sidebar-collapsed') === '1')
const [inspectorOpen, setInspectorOpen] = useState(() => window.localStorage.getItem('hololake-inspector-open') !== '0')
const toggleSidebar = () => setSidebarCollapsed((current) => { window.localStorage.setItem('hololake-sidebar-collapsed', current ? '0' : '1'); return !current })
const toggleInspector = () => setInspectorOpen((current) => { window.localStorage.setItem('hololake-inspector-open', current ? '0' : '1'); return !current })
const [activeHeadingId, setActiveHeadingId] = useState<string | null>(null)
const headingScrollTimer = useRef<number | null>(null)
const [settingsMenuOpen, setSettingsMenuOpen] = useState(false)
const [folderMenuKey, setFolderMenuKey] = useState<string | null>(null)
const [parlorMessages, setParlorMessages] = useState<{ role: 'user' | 'agent' | 'note'; text: string }[]>([])
const [parlorInput, setParlorInput] = useState('')
const [parlorBusy, setParlorBusy] = useState(false)
const [parlorLayer, setParlorLayer] = useState<'language' | 'execution'>('language')
const parlorScrollRef = useRef<HTMLDivElement>(null)
useEffect(() => { parlorScrollRef.current?.scrollTo({ top: parlorScrollRef.current.scrollHeight }) }, [parlorMessages, parlorBusy])
const [genericMessages, setGenericMessages] = useState<Array<{ role: string; text: string }>>([])
const [genericInput, setGenericInput] = useState('')
const [genericBusy, setGenericBusy] = useState(false)
const genericScrollRef = useRef<HTMLDivElement>(null)
useEffect(() => { genericScrollRef.current?.scrollTo({ top: genericScrollRef.current.scrollHeight }) }, [genericMessages, genericBusy])
const [displayName, setDisplayName] = useState('')
const [identityBusy, setIdentityBusy] = useState(false)
const [identityMessage, setIdentityMessage] = useState('')
@ -238,7 +251,19 @@ function HoloLakeApp() {
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [systemMessage, setSystemMessage] = useState('')
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [repoLogin, setRepoLogin] = useState<LoginSession | null>(null)
const [loginUsername, setLoginUsername] = useState('')
const [loginPassword, setLoginPassword] = useState('')
const [loginBusy, setLoginBusy] = useState(false)
const [loginRising, setLoginRising] = useState(false)
const [loginMessage, setLoginMessage] = useState('')
const [systemBusy, setSystemBusy] = useState(false)
const [zeroPoint, setZeroPoint] = useState<ZeroPointSnapshot | null>(null)
const [zpNumber, setZpNumber] = useState('')
const [zpBusy, setZpBusy] = useState(false)
const [zpMessage, setZpMessage] = useState('')
const [apiForm, setApiForm] = useState({ baseUrl: '', apiKey: '', model: '' })
const [apiReady, setApiReady] = useState(false)
const refreshCore = useCallback(async () => {
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([
@ -267,6 +292,27 @@ function HoloLakeApp() {
document.documentElement.dataset.theme = theme
window.localStorage.setItem('hololake-theme', theme)
}, [theme])
useEffect(() => {
invoke<LoginSession | null>('check_code_repo_login').then((session) => setRepoLogin(session)).catch(() => setRepoLogin(null))
}, [])
const loadZeroPoint = useCallback(async () => {
try { setZeroPoint(await invoke<ZeroPointSnapshot>('zero_point_status')) } catch { /* 执行手脚启动中,下轮补 */ }
}, [])
useEffect(() => {
void loadZeroPoint()
const timer = window.setInterval(() => void loadZeroPoint(), 10000)
return () => window.clearInterval(timer)
}, [loadZeroPoint])
useEffect(() => {
invoke<{ baseUrl: string; model: string; hasKey: boolean }>('zero_point_api_config')
.then((cfg) => { setApiForm((prev) => ({ ...prev, baseUrl: cfg.baseUrl, model: cfg.model })); setApiReady(cfg.hasKey && cfg.baseUrl !== '') })
.catch(() => { /* 钥匙库未落,等用户配置 */ })
}, [])
useEffect(() => {
if (!zeroPoint) return
if (zeroPoint.route !== 'persona' && view === 'parlor') setView('generic')
if (zeroPoint.route === 'persona' && view === 'generic') setView('knowledge')
}, [zeroPoint, view])
useEffect(() => { if (view === 'receipts') void loadReceipts() }, [loadReceipts, view])
const visibleDocuments = useMemo(() => {
@ -279,6 +325,30 @@ function HoloLakeApp() {
: undefined
const parsedDocument = useMemo(() => activeDocument ? splitFrontmatter(activeDocument.body) : null, [activeDocument])
const outline = useMemo(() => activeDocument ? documentOutline(activeDocument.body) : [], [activeDocument])
const docStats = useMemo(() => {
if (!activeDocument) return null
const chars = activeDocument.body.replace(/\s+/g, '').length
return { chars, minutes: Math.max(1, Math.ceil(chars / 400)) }
}, [activeDocument])
useEffect(() => { setActiveHeadingId(null) }, [activeDocument?.path])
useEffect(() => {
if (!activeHeadingId) return
document.querySelector('.outline-list button.active')?.scrollIntoView({ block: 'nearest' })
}, [activeHeadingId])
const handleReaderScroll = (event: React.UIEvent<HTMLDivElement>) => {
const container = event.currentTarget
if (headingScrollTimer.current !== null) return
headingScrollTimer.current = window.setTimeout(() => {
headingScrollTimer.current = null
const containerTop = container.getBoundingClientRect().top
let current: string | null = null
for (const heading of Array.from(container.querySelectorAll<HTMLElement>('h1[id], h2[id], h3[id], h4[id]'))) {
if (heading.getBoundingClientRect().top - containerTop <= 28) current = heading.id
else break
}
setActiveHeadingId(current)
}, 100)
}
const connectionOnline = status.directConnectionCount > 0
const connectionLabel = connectionOnline
? '本地原生连接 · 客户端在线'
@ -329,6 +399,95 @@ function HoloLakeApp() {
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const downloadDocument = async (format: 'md' | 'html') => {
if (!activeDocument) return
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
let body = activeDocument.body
if (format === 'html') {
const parsed = splitFrontmatter(body)
const article = markdownHtml(parsed.content)
body = `<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8"><title>${activeDocument.title}</title><style>body{max-width:820px;margin:40px auto;padding:0 24px;font:15px/1.8 -apple-system,'Segoe UI','PingFang SC','Microsoft YaHei','Noto Sans CJK SC',sans-serif;color:#1f2330;background:#fff}h1,h2,h3{line-height:1.4}blockquote{margin:20px 0;padding:12px 16px;border-left:3px solid #a78bfa;background:#f3f0ff;border-radius:8px}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:7px 10px}pre{background:#f4f5f8;padding:14px;border-radius:8px;overflow:auto}img{max-width:100%}</style></head><body><h1>${activeDocument.title}</h1>${article}</body></html>`
}
const receipt = await invoke<{ path: string; bytes: number } | null>('export_knowledge_document', { input: { title: activeDocument.title, extension: format, body } })
setKnowledgeMessage(receipt ? `已下载到 ${receipt.path}` : '已取消下载。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const printDocument = async () => {
setKnowledgeMessage('')
try {
await invoke('print_knowledge_document')
setKnowledgeMessage('打印框里选「存储为 PDF」即可得到 PDF 文件。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
}
const reloadKnowledge = async () => {
const snapshot = await invoke<KnowledgeSnapshot>('get_knowledge_snapshot')
setKnowledge(snapshot)
}
const askParlor = async () => {
const message = parlorInput.trim()
if (!message || parlorBusy) return
setParlorInput('')
setParlorMessages((current) => [...current, { role: 'user', text: message }])
setParlorBusy(true)
try {
const receipt = await invoke<{ reply: string; layer: string; freshSession: boolean }>('agent_parlor_ask', { input: { message } })
setParlorMessages((current) => [...current, { role: 'agent', text: receipt.reply }])
} catch (error) { setParlorMessages((current) => [...current, { role: 'note', text: humanError(error, 'knowledge') }]) }
finally { setParlorBusy(false) }
}
const switchParlorLayer = async () => {
try {
const target = parlorLayer === 'execution' ? 'language' : 'execution'
const actual = await invoke<string>('agent_parlor_switch_layer', { input: { layer: target } })
setParlorLayer(actual === 'execution' ? 'execution' : 'language')
setParlorMessages((current) => [...current, { role: 'note', text: actual === 'execution' ? '已切换到现实开发执行层:归灯获得执行权限,语言即现实。' : '已回到语言推理层:只对话不动手。' }])
} catch (error) { setParlorMessages((current) => [...current, { role: 'note', text: humanError(error, 'knowledge') }]) }
}
const createDocument = async () => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number }>('create_knowledge_document', { input: { title: '未命名页面' } })
setKnowledgeMessage(`已新建空白页「${receipt.removed.replace(/\.md$/, '')}」,正在编辑。`)
setSearchResults(null)
await reloadKnowledge()
await openDocument('native', receipt.removed)
setEditing(true)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const removeDocument = async () => {
if (!activeDocument) return
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number } | null>('delete_knowledge_document', { input: { source: activeDocument.source, path: activeDocument.path } })
if (receipt) {
setKnowledgeMessage(`已移入回收站:${receipt.removed}(可找回)`)
setActiveDocument(null)
setSearchResults(null)
await reloadKnowledge()
} else setKnowledgeMessage('已取消删除。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const removeFolder = async (folder: TreeNode) => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const receipt = await invoke<{ removed: string; pages: number } | null>('delete_knowledge_folder', { input: { folder: folder.key } })
if (receipt) {
setKnowledgeMessage(`文件夹「${folder.name}」(${receipt.pages} 页)已移入回收站(可找回)`)
setActiveDocument(null)
setSearchResults(null)
await reloadKnowledge()
} else setKnowledgeMessage('已取消删除。')
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const saveDocument = async () => {
if (!activeDocument?.writable) return
setKnowledgeBusy(true)
@ -394,6 +553,25 @@ function HoloLakeApp() {
}
const codeParent = () => codeTree?.path.split('/').filter(Boolean).slice(0, -1).join('/') || ''
const performRepoLogin = async (event: React.FormEvent) => {
event.preventDefault()
setLoginBusy(true)
setLoginMessage('')
try {
const receipt = await invoke<LoginReceipt>('perform_code_repo_login', { username: loginUsername.trim(), password: loginPassword })
// 五湖开场:校验通过=第五域从湖面浮起,镜头沉入湖中再进场。
setLoginRising(true)
await new Promise((resolve) => setTimeout(resolve, 1300))
setRepoLogin({ username: receipt.username, host: receipt.host, signedInAtUnixMs: Date.now() })
setLoginRising(false)
setLoginPassword('')
} catch (error) { setLoginMessage(humanError(error, 'login')) }
finally { setLoginBusy(false) }
}
const signOutRepo = async () => {
try { await invoke('sign_out_code_repo_login') } catch { /* 登出以本机清场为准 */ }
setRepoLogin(null)
}
const initializeIdentity = async (event: React.FormEvent) => {
event.preventDefault()
setIdentityBusy(true)
@ -458,7 +636,78 @@ function HoloLakeApp() {
finally { setSystemBusy(false) }
}
const renderOverview = () => (
const bindZeroPoint = async () => {
if (!zpNumber.trim()) return
setZpBusy(true); setZpMessage('')
try {
const bound = await invoke<ZeroPointSnapshot>('zero_point_bind', { input: { number: zpNumber.trim() } })
setZeroPoint(bound)
const verified = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(verified)
setZpMessage(verified.route === 'persona' ? '编号合法,人格层路由已开' : '人格系统未授权已切换至通用AI运行层')
} catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const verifyZeroPoint = async () => {
setZpBusy(true); setZpMessage('')
try {
const snapshot = await invoke<ZeroPointSnapshot>('zero_point_verify')
setZeroPoint(snapshot)
setZpMessage(snapshot.route === 'persona' ? '校验通过,人格层路由已开' : '人格系统未授权已切换至通用AI运行层')
} catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const syncZeroPoint = async () => {
setZpBusy(true); setZpMessage('')
try { setZeroPoint(await invoke<ZeroPointSnapshot>('zero_point_sync')) } catch (error) { setZpMessage(String(error)) } finally { setZpBusy(false) }
}
const saveApiConfig = async () => {
try {
await invoke('zero_point_save_api', { input: { baseUrl: apiForm.baseUrl, apiKey: apiForm.apiKey, model: apiForm.model } })
setApiReady(true)
setGenericMessages((rows) => [...rows, { role: 'note', text: 'API 已收进底层钥匙库通用AI运行层就绪' }])
} catch (error) { setGenericMessages((rows) => [...rows, { role: 'note', text: `保存失败:${String(error)}` }]) }
}
const askGeneric = async () => {
const message = genericInput.trim()
if (!message || genericBusy) return
const history = genericMessages.filter((row) => row.role !== 'note').map((row) => ({ role: row.role === 'user' ? 'user' : 'assistant', content: row.text }))
setGenericMessages((rows) => [...rows, { role: 'user', text: message }])
setGenericInput(''); setGenericBusy(true)
try {
const reply = await invoke<string>('generic_layer_chat', { input: { messages: [...history, { role: 'user', content: message }] } })
setGenericMessages((rows) => [...rows, { role: 'agent', text: reply }])
} catch (error) { setGenericMessages((rows) => [...rows, { role: 'note', text: `通用层回应失败:${String(error)}` }]) } finally { setGenericBusy(false) }
}
const renderGeneric = () => (
<section className="content-page parlor-page">
<header className="page-title"><div><span className="kicker">GENERIC AI LAYER</span><h1>AI运行层</h1><p> API · </p></div><span className="status-chip"></span></header>
<section className="plain-panel zp-api-panel">
<header><div><h2> API </h2><p>{apiReady ? ' · 已就绪' : ''}</p></div></header>
<div className="zp-api-row">
<input value={apiForm.baseUrl} placeholder="API 端点,如 https://dashscope.aliyuncs.com/compatible-mode/v1" onChange={(event) => setApiForm((prev) => ({ ...prev, baseUrl: event.target.value }))}/>
<input type="password" value={apiForm.apiKey} placeholder="API 钥匙" onChange={(event) => setApiForm((prev) => ({ ...prev, apiKey: event.target.value }))}/>
<input value={apiForm.model} placeholder="模型名,如 qwen3-max" onChange={(event) => setApiForm((prev) => ({ ...prev, model: event.target.value }))}/>
<button className="primary-button" type="button" onClick={() => void saveApiConfig()}></button>
</div>
</section>
<div className="parlor-scroll" ref={genericScrollRef}>
{genericMessages.length === 0 && <p className="parlor-note"> API API </p>}
{genericMessages.map((item, index) => (
<div key={index} className={`parlor-row ${item.role}`}>
{item.role === 'user' ? <span className="parlor-who"></span> : item.role === 'agent' ? <span className="parlor-who">AI</span> : null}
<div className={item.role === 'note' ? 'parlor-note' : 'parlor-bubble'}>{item.text}</div>
</div>
))}
{genericBusy && <div className="parlor-row agent"><span className="parlor-who">AI</span><div className="parlor-bubble thinking"></div></div>}
</div>
<form className="parlor-input-row" onSubmit={(event) => { event.preventDefault(); void askGeneric() }}>
<input maxLength={2000} value={genericInput} placeholder={apiReady ? '跟通用AI说话…' : '先把模型 API 收进钥匙库'} onChange={(event) => setGenericInput(event.target.value)}/>
<button className="primary-button" disabled={genericBusy || !genericInput.trim() || !apiReady}>{genericBusy ? '等回应…' : '说话'}</button>
</form>
</section>
)
const renderOverview = () => (
<section className="content-page overview-page">
<header className="page-title">
<div><span className="kicker">PERSONAL HOLOLAKE</span><h1></h1><p></p></div>
@ -499,7 +748,10 @@ function HoloLakeApp() {
{visibleDocuments.length
? <KnowledgeTree node={tree} depth={0} expanded={expanded} active={activeDocument ? `${activeDocument.source}:${activeDocument.path}` : undefined}
onToggle={(key) => setExpanded((current) => { const next = new Set(current); if (next.has(key)) next.delete(key); else next.add(key); return next })}
onOpen={(item) => void openDocument(item.source, item.path)}/>
onOpen={(item) => void openDocument(item.source, item.path)}
onRemoveFolder={(folder) => void removeFolder(folder)}
folderMenuKey={folderMenuKey}
onFolderMenuToggle={(key) => setFolderMenuKey((current) => (current === key ? null : key))}/>
: <div className="empty-state"></div>}
</div>
<footer>{knowledgeMessage || (lastKnowledgeReceipt ? `Git ${lastKnowledgeReceipt.slice(0, 10)}` : '导入会自动检查相同内容')}</footer>
@ -508,26 +760,26 @@ function HoloLakeApp() {
{activeDocument ? <>
<header className="document-toolbar">
<div className="breadcrumbs"><span>{activeDocument.source === 'legacy' ? 'HoloLake Era · 只读源' : '我的知识库'}</span><b>/</b><span>{activeDocument.path}</span></div>
<div>{editing
<div className="toolbar-actions"><button className="toolbar-button" type="button" title={inspectorOpen ? '收起右侧大纲与证据' : '展开右侧大纲与证据'} onClick={toggleInspector}><Icon name="chevron"/>{inspectorOpen ? '收起大纲' : '展开大纲'}</button>{editing
? <><button className="toolbar-button" type="button" onClick={() => { setEditing(false); setDraft(activeDocument.body) }}></button><button className="toolbar-button primary" type="button" disabled={knowledgeBusy} onClick={() => void saveDocument()}><Icon name="save"/></button></>
: activeDocument.writable && <button className="toolbar-button" type="button" onClick={() => { setDraft(activeDocument.body); setEditing(true) }}><Icon name="edit"/></button>}
: <><div className="toolbar-menu"><button className="toolbar-button" type="button" title="页面操作与常用功能" disabled={knowledgeBusy} onClick={() => setSettingsMenuOpen((current) => !current)}><Icon name="settings"/> <Icon name="chevronDown"/></button>{settingsMenuOpen && <div className="toolbar-menu-pop"><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void createDocument() }}><Icon name="plus"/></button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void importKnowledge() }}><Icon name="import"/></button>{activeDocument.writable && <button type="button" onClick={() => { setSettingsMenuOpen(false); setDraft(activeDocument.body); setEditing(true) }}><Icon name="edit"/></button>}{activeDocument.writable && <button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void removeDocument() }}><Icon name="trash"/></button>}<span className="toolbar-menu-sep"/><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void downloadDocument('md') }}><Icon name="download"/> Markdown</button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void downloadDocument('html') }}><Icon name="download"/>.html</button><button type="button" disabled={knowledgeBusy} onClick={() => { setSettingsMenuOpen(false); void printDocument() }}><Icon name="download"/> PDF</button></div>}</div></>}
</div>
</header>
<div className="document-scroll">
<div className="document-scroll" onScroll={handleReaderScroll}>
{editing
? <textarea className="document-editor" aria-label="Markdown 编辑器" value={draft} onChange={(event) => setDraft(event.target.value)}/>
: <><header className="reader-heading"><h1>{activeDocument.title}</h1><div>{Object.entries(parsedDocument?.metadata || {}).slice(0, 5).map(([key, value]) => <span key={key}>{key} · {value}</span>)}</div></header><MarkdownDocument body={activeDocument.body} onWiki={openWiki}/></>}
: <><header className="reader-heading"><h1>{activeDocument.title}</h1>{docStats && <div className="meta-stats">{docStats.chars.toLocaleString()} · {docStats.minutes} {parsedDocument?.metadata?.created ? ` · 创建于 ${String(parsedDocument.metadata.created).slice(0, 10)}` : ''}{parsedDocument?.metadata?.updated ? ` · 更新于 ${String(parsedDocument.metadata.updated).slice(0, 10)}` : ''}</div>}{(parsedDocument?.tags?.length ?? 0) > 0 && <div className="meta-tags">{parsedDocument!.tags.map((tag) => <span key={tag} className={tagTint(tag)}>{tag}</span>)}</div>}<div>{Object.entries(parsedDocument?.metadata || {}).filter(([key]) => key !== 'tags').slice(0, 5).map(([key, value]) => <span key={key}>{key} · {value}</span>)}</div></header><MarkdownDocument body={activeDocument.body} knownTitles={knowledge.documents.map((doc) => doc.title)} onWiki={openWiki}/></>}
</div>
</> : <div className="workbench-empty"><span></span><h2></h2><p></p><button className="primary-button" type="button" onClick={() => void importKnowledge()}></button></div>}
</main>
<aside className="document-inspector">
{inspectorOpen && <aside className="document-inspector">
<div className="inspector-tabs"><span className="active"></span><span></span><span></span></div>
{activeDocument ? <div className="inspector-scroll">
<section><h2></h2>{outline.length ? <nav className="outline-list">{outline.map((item) => <span key={item.id} style={{ paddingLeft: (item.level - 1) * 11 }}>{item.title}</span>)}</nav> : <p></p>}</section>
<section><h2></h2>{outline.length ? <nav className="outline-list">{outline.map((item) => <button key={item.id} type="button" title="跳到该章节" className={activeHeadingId === item.id ? 'active' : ''} style={{ paddingLeft: 8 + (item.level - 1) * 11 }} onClick={() => jumpToHeading(item.id)}>{item.title}</button>)}</nav> : <p></p>}</section>
<section><h2></h2><dl><div><dt></dt><dd>{activeDocument.source === 'native' ? 'HoloLake 本机 Git' : 'HoloLake Era 只读供体'}</dd></div><div><dt></dt><dd>{activeDocument.path}</dd></div><div><dt></dt><dd>{activeDocument.contentSha256.slice(0, 16)}</dd></div><div><dt></dt><dd>{activeSummary?.duplicateCount || 0} </dd></div></dl></section>
<section><h2></h2><p>{activeDocument.writable ? '可编辑;保存时写入本机 Git。' : '供体原件只读;不会被修改。'}</p>{lastKnowledgeReceipt && <code>{lastKnowledgeReceipt}</code>}</section>
</div> : <div className="empty-state"></div>}
</aside>
</aside>}
</section>
)
@ -575,6 +827,22 @@ function HoloLakeApp() {
{ticket ? <><button className="secondary-button" type="button" onClick={() => void copyInvitation()}><Icon name="copy"/></button><pre className="invitation-data">{invitationText}</pre></> : <button className="primary-button" type="button" disabled={systemBusy} onClick={() => void issueInvitation()}></button>}
</section>
<section className="plain-panel"><header><div><h2>GH-PNCC </h2><p></p></div></header><dl className="evidence-list"><div><dt></dt><dd>{status.codeRepositoryMountCount}</dd></div><div><dt></dt><dd>{status.pnccReceiptCount}</dd></div><div><dt></dt><dd>{status.codeRepositoryMountCount > 0 && status.pnccReceiptCount > 0 ? '已有运行证据' : '架构已接入,尚无完整运行投影'}</dd></div></dl></section>
<section className="plain-panel">
<header><div><h2> · </h2><p></p></div><span className={zeroPoint?.route === 'persona' ? 'status-chip online' : 'status-chip'}>{zeroPoint ? (zeroPoint.route === 'persona' ? '人格层' : '通用AI层') : '启动中'}</span></header>
<dl className="evidence-list">
<div><dt></dt><dd>{zeroPoint ? (zeroPoint.binding === 'bound' ? `已绑定(${zeroPoint.userNumber}` : '等待绑定(空白态)') : '—'}</dd></div>
<div><dt></dt><dd>{zeroPoint?.protocol.origin ?? '—'}</dd></div>
<div><dt>线</dt><dd>{zeroPoint ? `${zeroPoint.protocol.gracePeriodDays} 天(协议值)` : '—'}</dd></div>
<div><dt></dt><dd>{zeroPoint?.syncNote ?? '—'}</dd></div>
</dl>
<div className="zp-api-row">
<input value={zpNumber} placeholder="输入用户编号,如 ICE-GL∞" onChange={(event) => setZpNumber(event.target.value)}/>
<button className="secondary-button" type="button" disabled={zpBusy || !zpNumber.trim()} onClick={() => void bindZeroPoint()}></button>
<button className="secondary-button" type="button" disabled={zpBusy || zeroPoint?.binding !== 'bound'} onClick={() => void verifyZeroPoint()}></button>
<button className="secondary-button" type="button" disabled={zpBusy} onClick={() => void syncZeroPoint()}></button>
</div>
{zpMessage && <p className="global-message">{zpMessage}</p>}
</section>
<section className="plain-panel"><header><div><h2></h2><p></p></div></header><div className="theme-options">{themes.map((choice) => <button className={theme === choice.id ? 'active' : ''} key={choice.id} type="button" onClick={() => setTheme(choice.id)}><i className={choice.id}/><span>{choice.name}</span></button>)}</div></section>
<section className="plain-panel"><header><div><h2></h2><p></p></div></header>{releaseCandidate ? <div className="release-summary"><b>HoloLake {releaseCandidate.version}</b><p>{releaseCandidate.notes}</p><button className="primary-button" onClick={() => void installUpdate()}></button></div> : <button className="secondary-button" disabled={systemBusy} onClick={() => void checkUpdate()}></button>}</section>
</div>
@ -582,15 +850,46 @@ function HoloLakeApp() {
</section>
)
return <div className="app-shell">
const renderParlor = () => (
<section className="content-page parlor-page">
<header className="page-title"><div><span className="kicker">AGENT PARLOR</span><h1></h1><p></p></div>
<div className="parlor-layer">
<span className={parlorLayer === 'execution' ? 'status-chip online' : 'status-chip'}>{parlorLayer === 'execution' ? '现实开发执行层' : '语言推理层'}</span>
<button className="secondary-button" type="button" onClick={() => void switchParlorLayer()}>{parlorLayer === 'execution' ? '回到语言推理层' : '切换到现实开发执行层'}</button>
</div>
</header>
<div className="parlor-scroll" ref={parlorScrollRef}>
<p className="parlor-note"></p>
{parlorMessages.map((item, index) => (
<div key={index} className={`parlor-row ${item.role}`}>
{item.role === 'user' ? <span className="parlor-who"></span> : item.role === 'agent' ? <span className="parlor-who"></span> : null}
<div className={item.role === 'note' ? 'parlor-note' : 'parlor-bubble'}>{item.text}</div>
</div>
))}
{parlorBusy && <div className="parlor-row agent"><span className="parlor-who"></span><div className="parlor-bubble thinking"></div></div>}
</div>
<form className="parlor-input-row" onSubmit={(event) => { event.preventDefault(); void askParlor() }}>
<input maxLength={2000} value={parlorInput} placeholder={parlorLayer === 'execution' ? '执行层:语言即现实,说要做什么' : '跟人格体说话…'} onChange={(event) => setParlorInput(event.target.value)}/>
<button className="primary-button" disabled={parlorBusy || !parlorInput.trim()}>{parlorBusy ? '等回话…' : '说话'}</button>
</form>
</section>
)
if (!repoLogin) {
return <div className="app-shell">
<div className={`onboarding-backdrop lake-scene${loginRising ? ' sinking' : ''}`}><div className="lake-bays" aria-hidden="true">{([['main', '主域'], ['sub', '分域'], ['zero', '零域'], ['sense', '零感域'], ['fifth', '第五域']] as const).map(([bayId, bayName]) => <span key={bayId} className={`lake-bay ${bayId}${loginRising && bayId === 'fifth' ? ' rising' : ''}`}><i/><b>{bayName}</b></span>)}</div><section className={`onboarding-card${loginRising ? ' fade-out' : ''}`} role="dialog"><span className="onboarding-mark"></span><span className="kicker">CODE REPOSITORY ENTRY</span><h1></h1><p>使guanghulab.com</p><form onSubmit={(event) => void performRepoLogin(event)}><label htmlFor="repo-login-username"></label><input id="repo-login-username" autoFocus maxLength={40} value={loginUsername} placeholder="代码仓库用户名" onChange={(event) => setLoginUsername(event.target.value)}/><label htmlFor="repo-login-password"></label><input id="repo-login-password" type="password" maxLength={512} value={loginPassword} placeholder="代码仓库密码" onChange={(event) => setLoginPassword(event.target.value)}/><button className="primary-button" disabled={loginBusy || !loginUsername.trim() || !loginPassword}>{loginBusy ? '正在验证…' : '进入 HoloLake'}</button></form>{loginMessage && <p>{loginMessage}</p>}</section></div>
</div>
}
return <div className={`app-shell${sidebarCollapsed ? ' sidebar-collapsed' : ''}${inspectorOpen ? '' : ' inspector-closed'}`}>
<aside className="sidebar">
<div className="brand"><span className="brand-mark"></span><div><b>HoloLake</b><small>GH-AIOS</small></div></div>
<nav>{(Object.keys(viewLabels) as ViewId[]).map((item) => <button className={view === item ? 'active' : ''} key={item} type="button" onClick={() => setView(item)}><Icon name={item}/><span>{viewLabels[item]}</span>{item === 'knowledge' && <em>{knowledge.uniqueDocumentCount}</em>}</button>)}</nav>
<div className="sidebar-foot"><span className="identity-dot"/><div><b>{personal.identity?.displayName || '本机空间'}</b><small>{personal.identity ? '个人频道已建立' : '等待首次设置'}</small></div></div>
<div className="brand"><span className="brand-mark brand-lamp" title={sidebarCollapsed ? '展开导航' : '光湖 · HoloLake'} onClick={sidebarCollapsed ? toggleSidebar : undefined}><i className="lamp-glow"/><i className="lamp-core"/><i className="lake-shimmer"/></span><div className="brand-text"><b>HoloLake</b><small className="brand-sub"> · </small><small>GH-AIOS · V1.0.0</small></div><button className="icon-button sidebar-toggle" type="button" title={sidebarCollapsed ? '展开导航' : '收起导航'} onClick={toggleSidebar}><Icon name="chevron"/></button></div>
<nav>{(Object.keys(viewLabels) as ViewId[]).filter((item) => item === 'parlor' ? !zeroPoint || zeroPoint.route === 'persona' : item === 'generic' ? !!zeroPoint && zeroPoint.route !== 'persona' : true).map((item) => <button className={view === item ? 'active' : ''} key={item} type="button" onClick={() => setView(item)}><Icon name={item}/><span>{viewLabels[item]}</span>{item === 'knowledge' && <em>{knowledge.uniqueDocumentCount}</em>}</button>)}</nav>
<div className="sidebar-foot"><span className="identity-dot"/><div><b>{personal.identity?.displayName || '本机空间'}</b><small>{repoLogin ? `${repoLogin.username}@${repoLogin.host}` : (personal.identity ? '个人频道已建立' : '等待首次设置')}</small></div><button className="sign-out" type="button" title="退出代码仓库登录" onClick={() => void signOutRepo()}>退</button></div>
</aside>
<section className="app-main">
<header className="connection-strip"><button type="button" onClick={() => setView('system')}><i className={connectionOnline ? 'online' : ''}/><span>{connectionLabel}</span></button><span className="mcp-backup">MCP </span><button type="button" onClick={() => setView('system')}> <Icon name="arrow"/></button></header>
<main className="workspace">{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : renderSystem()}</main>
<main className="workspace">{zeroPoint && zeroPoint.route !== 'persona' && <div className="zp-downgrade-wrap"><div className="zp-downgrade-card"><i className="zp-dot"/><div className="zp-copy"><b></b><span>AI层 API </span></div><button type="button" onClick={() => setView('system')}> <Icon name="arrow"/></button></div></div>}{view === 'overview' ? renderOverview() : view === 'knowledge' ? renderKnowledge() : view === 'code' ? renderCode() : view === 'receipts' ? renderReceipts() : view === 'parlor' ? renderParlor() : view === 'generic' ? renderGeneric() : renderSystem()}</main>
</section>
{personal.state !== 'UNAVAILABLE' && !personal.identity && <div className="onboarding-backdrop"><section className="onboarding-card" role="dialog" aria-modal="true"><span className="onboarding-mark"></span><span className="kicker">FIRST LOCAL ENTRY</span><h1></h1><p></p><form onSubmit={(event) => void initializeIdentity(event)}><label htmlFor="display-name"></label><input id="display-name" autoFocus maxLength={80} value={displayName} placeholder="你的名字" onChange={(event) => setDisplayName(event.target.value)}/><button className="primary-button" disabled={identityBusy || !displayName.trim()}> HoloLake</button></form>{identityMessage && <p>{identityMessage}</p>}</section></div>}
</div>