hololake-system-architecture/product-source/hololake-native-desktop/src/main.tsx

599 lines
40 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 { StrictMode, useCallback, useEffect, useMemo, 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 './design-tokens.css'
import './styles.css'
type ThemeId = 'night' | 'dawn' | 'nebula' | 'candle' | 'clear'
type ViewId = 'overview' | 'knowledge' | 'code' | 'receipts' | 'system'
type KnowledgeSource = 'native' | 'legacy'
interface HomeStatus {
directLocalBrokerState: string
directConnectionCount: number
resumableSessionCount: number
codeRepositoryMountCount: number
pnccReceiptCount: number
updateState: string
releaseRecoveryState: string
mcpRole: string
}
interface PersonalChannelIdentity { humanSubjectId: string; displayName: string; channelId: string; createdAtUnixMs: number }
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 {
source: KnowledgeSource
path: string
title: string
updatedAtUnixMs: number
sizeBytes: number
contentSha256: string
duplicateCount: number
}
interface KnowledgeSnapshot {
state: string
nativeRoot: string
legacyAvailable: boolean
legacyRoot?: string
documents: KnowledgeDocumentSummary[]
rawDocumentCount: number
uniqueDocumentCount: number
duplicateDocumentCount: number
truncated: boolean
}
interface KnowledgeDocument {
source: KnowledgeSource
path: string
title: string
body: string
updatedAtUnixMs: number
contentSha256: string
writable: boolean
}
interface KnowledgeSearchResult { source: KnowledgeSource; path: string; title: string; snippet: string }
interface KnowledgeImportResult {
state: string
sourceName: string
importedDocuments: number
importedAssets: number
existingDocuments: number
conflicts: number
skipped: number
firstDocument?: string
gitCommit: string
snapshot: KnowledgeSnapshot
}
interface KnowledgeSaveResult { state: string; gitCommit: string; document: KnowledgeDocument }
interface CodeChannelEntry { channelId: string; name: string; sourceKind: string; localPath: string; remoteUrl?: string; gitHead: string; branch: string; repositoryClean: boolean; registeredAtUnixMs: number }
interface CodeChannelSnapshot { state: string; channels: CodeChannelEntry[]; authority: string }
interface CodeTreeEntry { path: string; name: string; kind: 'directory' | 'file'; sizeBytes: number }
interface CodeTreeSnapshot { channelId: string; path: string; entries: CodeTreeEntry[]; truncated: boolean }
interface CodeFileProjection { channelId: string; path: string; format: string; source: string; humanMarkdown: string; sizeBytes: number }
interface ReceiptEvent { sequence: number; kind: string; observedAtUnixMs: number; eventHash: string }
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 }
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 } }
const previewKnowledge: KnowledgeSnapshot = { state: 'UNAVAILABLE', nativeRoot: '', legacyAvailable: false, documents: [], rawDocumentCount: 0, uniqueDocumentCount: 0, duplicateDocumentCount: 0, truncated: false }
const previewCode: CodeChannelSnapshot = { state: 'UNAVAILABLE', channels: [], authority: 'LOCAL_SOURCE_ACCESS_ONLY_NO_PUSH_OR_DEPLOY_AUTHORITY' }
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: '系统详情' }
function getOrCreateLocalId(key: string, prefix: string) {
const existing = window.localStorage.getItem(key)
if (existing) return existing
const generated = `${prefix}-${crypto.randomUUID()}`
window.localStorage.setItem(key, generated)
return generated
}
function humanError(error: unknown, context: 'knowledge' | 'code' | 'identity' | 'system') {
const value = String(error)
if (value.includes('HOST_NOT_TRUSTED')) return '该地址不属于已登记的光湖代码频道。'
if (value.includes('URL_INVALID')) return '频道地址无效,请粘贴完整的 HTTPS 克隆地址。'
if (value.includes('DESTINATION_EXISTS')) return '该代码频道已经存在,无需再次克隆。'
if (value.includes('CLONE_FAILED')) return '代码频道克隆失败,请核对地址及读取权限。'
if (value.includes('NO_SUPPORTED_FILES')) return '文件夹中没有可导入的知识文件。'
if (value.includes('SAVE_CONFLICT')) return '页面已在别处更新,请重新打开后再编辑。'
if (value.includes('FILE_UNSUPPORTED')) 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' }) {
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"/></>,
code: <><path d="m8.5 8-4 4 4 4M15.5 8l4 4-4 4M13.5 5l-3 14"/></>,
receipts: <><path d="M7 3.5h10v17l-2.5-1.5L12 20.5 9.5 19 7 20.5Z"/><path d="M10 8h4M10 12h4"/></>,
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"/></>,
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"/></>,
arrow: <><path d="M5 12h13M14 7l5 5-5 5"/></>,
chevron: <path d="m9 6 6 6-6 6"/>,
edit: <><path d="m4 20 4.5-1 10-10-3.5-3.5-10 10Z"/><path d="m13.5 6.5 3.5 3.5"/></>,
save: <><path d="M5 4h12l2 2v14H5Z"/><path d="M8 4v6h8V4M8 20v-6h8v6"/></>,
back: <path d="m15 6-6 6 6 6"/>,
}
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[]) {
const root: TreeNode = { name: '知识', key: '', folders: new Map(), documents: [] }
for (const document of documents) {
const segments = document.path.split('/').filter(Boolean)
const file = segments.pop()
let cursor = root
for (const segment of segments) {
const key = cursor.key ? `${cursor.key}/${segment}` : segment
if (!cursor.folders.has(segment)) cursor.folders.set(segment, { name: segment, key, folders: new Map(), documents: [] })
cursor = cursor.folders.get(segment)!
}
cursor.documents.push({ ...document, title: document.title || file || '未命名页面' })
}
return root
}
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 }) {
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 <>
{folders.map((folder) => {
const open = depth === 0 || expanded.has(folder.key)
return <div key={folder.key} className="tree-branch">
<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}/>}
</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)}>
<Icon name="file"/><span>{document.title}</span>{document.duplicateCount > 0 && <em title="折叠的相同内容">+{document.duplicateCount}</em>}
</button>)}
</>
}
function HoloLakeApp() {
const [theme, setTheme] = useState<ThemeId>(() => (window.localStorage.getItem('hololake-theme') as ThemeId) || 'night')
const [view, setView] = useState<ViewId>('knowledge')
const [status, setStatus] = useState<HomeStatus>(previewStatus)
const [personal, setPersonal] = useState<PersonalChannelSnapshot>(previewPersonal)
const [knowledge, setKnowledge] = useState<KnowledgeSnapshot>(previewKnowledge)
const [codeChannels, setCodeChannels] = useState<CodeChannelSnapshot>(previewCode)
const [activeDocument, setActiveDocument] = useState<KnowledgeDocument | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<KnowledgeSearchResult[] | null>(null)
const [knowledgeBusy, setKnowledgeBusy] = useState(false)
const [knowledgeMessage, setKnowledgeMessage] = useState('')
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['导入']))
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
const [lastKnowledgeReceipt, setLastKnowledgeReceipt] = useState('')
const [cloneUrl, setCloneUrl] = useState('')
const [codeBusy, setCodeBusy] = useState(false)
const [codeMessage, setCodeMessage] = useState('')
const [activeChannel, setActiveChannel] = useState<CodeChannelEntry | null>(null)
const [codeTree, setCodeTree] = useState<CodeTreeSnapshot | null>(null)
const [activeCodeFile, setActiveCodeFile] = useState<CodeFileProjection | null>(null)
const [codeMode, setCodeMode] = useState<'human' | 'source'>('human')
const [displayName, setDisplayName] = useState('')
const [identityBusy, setIdentityBusy] = useState(false)
const [identityMessage, setIdentityMessage] = useState('')
const [receipts, setReceipts] = useState<ReceiptEvent[]>([])
const [ticket, setTicket] = useState<DiscoveryTicketReceipt | null>(null)
const [systemMessage, setSystemMessage] = useState('')
const [releaseCandidate, setReleaseCandidate] = useState<ReleaseCandidate | null>(null)
const [systemBusy, setSystemBusy] = useState(false)
const refreshCore = useCallback(async () => {
const [homeResult, personalResult, knowledgeResult, codeResult] = await Promise.allSettled([
invoke<HomeStatus>('get_hololake_home_status'),
invoke<PersonalChannelSnapshot>('get_personal_channel_snapshot'),
invoke<KnowledgeSnapshot>('get_knowledge_snapshot'),
invoke<CodeChannelSnapshot>('get_code_channel_snapshot'),
])
if (homeResult.status === 'fulfilled') setStatus(homeResult.value)
if (personalResult.status === 'fulfilled') setPersonal(personalResult.value)
if (knowledgeResult.status === 'fulfilled') setKnowledge(knowledgeResult.value)
if (codeResult.status === 'fulfilled') setCodeChannels(codeResult.value)
}, [])
const loadReceipts = useCallback(async () => {
try {
const projection = await invoke<ReceiptProjection>('query_pncc_receipt_projection', { input: { afterSequence: 0, limit: 25 } })
setReceipts(projection.events)
} catch { setReceipts([]) }
}, [])
useEffect(() => {
void refreshCore()
const timer = window.setInterval(() => void refreshCore(), 3500)
return () => window.clearInterval(timer)
}, [refreshCore])
useEffect(() => {
document.documentElement.dataset.theme = theme
window.localStorage.setItem('hololake-theme', theme)
}, [theme])
useEffect(() => { if (view === 'receipts') void loadReceipts() }, [loadReceipts, view])
const visibleDocuments = useMemo(() => {
if (!searchResults) return knowledge.documents
return searchResults.map((result) => knowledge.documents.find((item) => item.source === result.source && item.path === result.path)).filter(Boolean) as KnowledgeDocumentSummary[]
}, [knowledge.documents, searchResults])
const tree = useMemo(() => knowledgeTree(visibleDocuments), [visibleDocuments])
const activeSummary = activeDocument
? knowledge.documents.find((item) => item.source === activeDocument.source && item.path === activeDocument.path)
: undefined
const parsedDocument = useMemo(() => activeDocument ? splitFrontmatter(activeDocument.body) : null, [activeDocument])
const outline = useMemo(() => activeDocument ? documentOutline(activeDocument.body) : [], [activeDocument])
const connectionOnline = status.directConnectionCount > 0
const connectionLabel = connectionOnline
? '本地原生连接 · 客户端在线'
: status.resumableSessionCount > 0
? '原生会话可续接 · 当前离线'
: '原生接口已驻留 · 尚无客户端'
const openDocument = async (source: KnowledgeSource, path: string) => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
setEditing(false)
try {
const document = await invoke<KnowledgeDocument>('read_knowledge_document', { input: { source, path } })
setActiveDocument(document)
setDraft(document.body)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const openWiki = (target: string) => {
const normalized = target.replace(/\.md$/i, '').toLowerCase()
const match = knowledge.documents.find((item) =>
item.title.toLowerCase() === normalized || item.path.replace(/\.md$/i, '').toLowerCase().endsWith(normalized))
if (match) void openDocument(match.source, match.path)
else setKnowledgeMessage(`未找到链接页面:${target}`)
}
const runSearch = async (event: React.FormEvent) => {
event.preventDefault()
if (!searchQuery.trim()) { setSearchResults(null); return }
setKnowledgeBusy(true)
try {
setSearchResults(await invoke<KnowledgeSearchResult[]>('search_knowledge', { input: { query: searchQuery } }))
setActiveDocument(null)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const importKnowledge = async () => {
setKnowledgeBusy(true)
setKnowledgeMessage('')
try {
const result = await invoke<KnowledgeImportResult | null>('select_and_import_knowledge_folder')
if (!result) return
setKnowledge(result.snapshot)
setSearchResults(null)
setSearchQuery('')
setLastKnowledgeReceipt(result.gitCommit)
setKnowledgeMessage(`导入完成:新增 ${result.importedDocuments},已存在 ${result.existingDocuments},冲突 ${result.conflicts}`)
if (result.firstDocument) await openDocument('native', result.firstDocument)
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const saveDocument = async () => {
if (!activeDocument?.writable) return
setKnowledgeBusy(true)
try {
const result = await invoke<KnowledgeSaveResult>('save_knowledge_document', {
input: { path: activeDocument.path, body: draft, expectedContentSha256: activeDocument.contentSha256 },
})
setActiveDocument(result.document)
setDraft(result.document.body)
setEditing(false)
setLastKnowledgeReceipt(result.gitCommit)
setKnowledgeMessage(result.state === 'UNCHANGED' ? '内容没有变化。' : '页面已保存,并写入本机 Git 回执。')
await refreshCore()
} catch (error) { setKnowledgeMessage(humanError(error, 'knowledge')) }
finally { setKnowledgeBusy(false) }
}
const cloneCodeChannel = async (event: React.FormEvent) => {
event.preventDefault()
if (!cloneUrl.trim()) return
setCodeBusy(true)
setCodeMessage('')
try {
const snapshot = await invoke<CodeChannelSnapshot>('clone_code_channel', { input: { url: cloneUrl } })
setCodeChannels(snapshot)
setCloneUrl('')
setCodeMessage('代码频道已克隆并登记。')
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const selectLocalCodeChannel = async () => {
setCodeBusy(true)
setCodeMessage('')
try {
const snapshot = await invoke<CodeChannelSnapshot | null>('select_local_code_channel')
if (snapshot) {
setCodeChannels(snapshot)
setCodeMessage('本地代码频道已登记,原文件未移动。')
}
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const browseChannel = async (channel: CodeChannelEntry, path = '') => {
setCodeBusy(true)
setActiveChannel(channel)
setActiveCodeFile(null)
try {
setCodeTree(await invoke<CodeTreeSnapshot>('browse_code_channel', { input: { channelId: channel.channelId, path } }))
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const openCodeEntry = async (entry: CodeTreeEntry) => {
if (!activeChannel) return
if (entry.kind === 'directory') { await browseChannel(activeChannel, entry.path); return }
setCodeBusy(true)
setCodeMode('human')
try {
setActiveCodeFile(await invoke<CodeFileProjection>('read_code_channel_file', {
input: { channelId: activeChannel.channelId, path: entry.path },
}))
} catch (error) { setCodeMessage(humanError(error, 'code')) }
finally { setCodeBusy(false) }
}
const codeParent = () => codeTree?.path.split('/').filter(Boolean).slice(0, -1).join('/') || ''
const initializeIdentity = async (event: React.FormEvent) => {
event.preventDefault()
setIdentityBusy(true)
try {
setPersonal(await invoke<PersonalChannelSnapshot>('initialize_personal_channel', { input: { displayName } }))
setDisplayName('')
} catch (error) { setIdentityMessage(humanError(error, 'identity')) }
finally { setIdentityBusy(false) }
}
const issueInvitation = async () => {
setSystemBusy(true)
setSystemMessage('')
try {
setTicket(await invoke<DiscoveryTicketReceipt>('issue_direct_local_discovery_ticket', {
input: {
accountId: getOrCreateLocalId('hololake-local-account', 'human-local'),
laneId: 'personal-channel',
clientInstanceId: getOrCreateLocalId('hololake-external-ai-client', 'external-ai'),
},
}))
setSystemMessage('一次性发现凭据已生成。连接成功后,状态条会显示真实在线数。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const invitationText = ticket ? JSON.stringify({
schema: 'hololake.direct-local-invitation/v1',
connectorArgument: '--connector',
openSession: {
operation: 'OPEN_SESSION',
input: {
accountId: window.localStorage.getItem('hololake-local-account'),
laneId: ticket.laneId,
clientInstanceId: ticket.clientInstanceId,
discoveryTicket: ticket.discoveryTicket,
},
},
}, null, 2) : ''
const copyInvitation = async () => {
try {
await navigator.clipboard.writeText(invitationText)
setSystemMessage('连接凭据已复制。')
} catch { setSystemMessage('复制失败,请手动选择凭据。') }
}
const checkUpdate = async () => {
setSystemBusy(true)
try {
const result = await invoke<ReleaseCheckReceipt>('check_hololake_update')
setReleaseCandidate(result.candidate || null)
setSystemMessage(result.candidate ? '发现已签名更新。' : '当前没有可安装更新。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const installUpdate = async () => {
if (!releaseCandidate) return
setSystemBusy(true)
try {
await invoke('confirm_hololake_update_install', {
input: { candidateId: releaseCandidate.candidateId, confirmationToken: releaseCandidate.confirmationToken },
})
setSystemMessage('更新安装已启动。')
} catch (error) { setSystemMessage(humanError(error, 'system')) }
finally { setSystemBusy(false) }
}
const renderOverview = () => (
<section className="content-page overview-page">
<header className="page-title">
<div><span className="kicker">PERSONAL HOLOLAKE</span><h1></h1><p></p></div>
<button className="evidence-pill" type="button" onClick={() => setView('system')}><i className={connectionOnline ? 'online' : ''}/>{connectionLabel}</button>
</header>
<div className="overview-grid">
<button className="metric-panel" type="button" onClick={() => setView('knowledge')}><span></span><strong>{knowledge.uniqueDocumentCount}</strong><small>{knowledge.duplicateDocumentCount} </small></button>
<button className="metric-panel" type="button" onClick={() => setView('code')}><span></span><strong>{codeChannels.channels.length}</strong><small></small></button>
<button className="metric-panel" type="button" onClick={() => setView('receipts')}><span></span><strong>{personal.integrity.receiptCount + status.pnccReceiptCount}</strong><small></small></button>
</div>
<div className="overview-columns">
<section className="plain-panel recent-panel">
<header><div><h2></h2><p></p></div><button type="button" onClick={() => setView('knowledge')}></button></header>
{knowledge.documents.slice().sort((a, b) => b.updatedAtUnixMs - a.updatedAtUnixMs).slice(0, 7).map((item) =>
<button className="recent-row" key={`${item.source}:${item.path}`} type="button" onClick={() => { setView('knowledge'); void openDocument(item.source, item.path) }}>
<Icon name="file"/><span><b>{item.title}</b><small>{item.path}</small></span><time>{new Date(item.updatedAtUnixMs).toLocaleDateString('zh-CN')}</time>
</button>)}
</section>
<section className="plain-panel system-proof">
<header><div><h2></h2><p>使</p></div></header>
<dl><div><dt>Unix </dt><dd>{status.directLocalBrokerState}</dd></div><div><dt>线</dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{status.resumableSessionCount}</dd></div><div><dt>PNCC </dt><dd>{status.pnccReceiptCount}</dd></div><div><dt>MCP</dt><dd> / </dd></div></dl>
<button className="secondary-button" type="button" onClick={() => setView('system')}></button>
</section>
</div>
</section>
)
const renderKnowledge = () => (
<section className="full-workbench knowledge-page">
<aside className="knowledge-browser">
<header><div><span className="kicker">KNOWLEDGE</span><h1></h1></div><button className="icon-button" title="导入文件夹" type="button" disabled={knowledgeBusy} onClick={() => void importKnowledge()}><Icon name="import"/></button></header>
<form className="search-box" onSubmit={(event) => void runSearch(event)}>
<Icon name="search"/><input aria-label="检索知识" value={searchQuery} placeholder="检索标题与正文" onChange={(event) => setSearchQuery(event.target.value)}/>
{searchResults && <button type="button" onClick={() => { setSearchResults(null); setSearchQuery('') }}></button>}
</form>
<div className="knowledge-counts"><span>{knowledge.uniqueDocumentCount} </span><span>{knowledge.duplicateDocumentCount} </span></div>
<div className="knowledge-tree" aria-busy={knowledgeBusy}>
{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)}/>
: <div className="empty-state"></div>}
</div>
<footer>{knowledgeMessage || (lastKnowledgeReceipt ? `Git ${lastKnowledgeReceipt.slice(0, 10)}` : '导入会自动检查相同内容')}</footer>
</aside>
<main className="document-workspace">
{activeDocument ? <>
<header className="document-toolbar">
<div className="breadcrumbs"><span>{activeDocument.source === 'legacy' ? 'HoloLake Era · 只读源' : '我的知识库'}</span><b>/</b><span>{activeDocument.path}</span></div>
<div>{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>
</header>
<div className="document-scroll">
{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}/></>}
</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">
<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><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>
</section>
)
const renderCode = () => (
<section className="full-workbench code-workbench">
<aside className="code-channels">
<header><div><span className="kicker">CODE CHANNEL</span><h1></h1></div></header>
<form className="clone-form" onSubmit={(event) => void cloneCodeChannel(event)}>
<label htmlFor="clone-url"></label><div><input id="clone-url" type="url" value={cloneUrl} placeholder="https://guanghulab.com/code/…" onChange={(event) => setCloneUrl(event.target.value)}/><button disabled={!cloneUrl.trim() || codeBusy}></button></div>
</form>
<button className="local-folder-button" type="button" onClick={() => void selectLocalCodeChannel()}><Icon name="folder"/> Git </button>
<div className="channel-selector">{codeChannels.channels.map((channel) => <button className={activeChannel?.channelId === channel.channelId ? 'active' : ''} key={channel.channelId} type="button" onClick={() => void browseChannel(channel)}><Icon name="code"/><span><b>{channel.name}</b><small>{channel.branch} · {channel.gitHead.slice(0, 8)}</small></span></button>)}</div>
<footer>{codeMessage || '仅提供本机读取,不授予推送、发布或部署权限。'}</footer>
</aside>
<aside className="repository-tree">
<header><button className="icon-button" disabled={!codeTree?.path} type="button" onClick={() => activeChannel && void browseChannel(activeChannel, codeParent())}><Icon name="back"/></button><div><b>{activeChannel?.name || '尚未选择频道'}</b><small>/{codeTree?.path || ''}</small></div></header>
<div className="repository-entries">{codeTree?.entries.map((entry) => <button key={entry.path} type="button" onClick={() => void openCodeEntry(entry)}><Icon name={entry.kind === 'directory' ? 'folder' : 'file'}/><span>{entry.name}</span>{entry.kind === 'directory' && <Icon name="chevron"/>}</button>) || <div className="empty-state"></div>}</div>
</aside>
<main className="code-reader">
{activeCodeFile ? <>
<header><div><span>{activeCodeFile.path}</span><small>{activeCodeFile.format.toUpperCase()} · {activeCodeFile.sizeBytes.toLocaleString()} bytes</small></div><div className="mode-switch"><button className={codeMode === 'human' ? 'active' : ''} type="button" onClick={() => setCodeMode('human')}></button><button className={codeMode === 'source' ? 'active' : ''} type="button" onClick={() => setCodeMode('source')}></button></div></header>
<div className="code-reader-scroll">{codeMode === 'human' ? <MarkdownDocument body={activeCodeFile.humanMarkdown}/> : <pre className="source-code"><code>{activeCodeFile.source}</code></pre>}</div>
</> : <div className="workbench-empty"><span>&lt;/&gt;</span><h2></h2><p> MarkdownJSONHDLPYAMLTOML </p></div>}
</main>
</section>
)
const renderReceipts = () => (
<section className="content-page">
<header className="page-title"><div><span className="kicker">LOCAL RECEIPTS</span><h1></h1><p></p></div></header>
<div className="receipt-grid">
<section className="plain-panel"><h2></h2>{personal.recentEvents.map((event) => <article className="receipt-row" key={event.eventId}><span>{String(event.sequence).padStart(2, '0')}</span><div><b>{event.summary}</b><small>{new Date(event.occurredAtUnixMs).toLocaleString('zh-CN')} · {event.receiptHash.slice(0, 12)}</small></div></article>)}</section>
<section className="plain-panel"><h2>PNCC </h2>{receipts.length ? receipts.map((receipt) => <article className="receipt-row" key={receipt.sequence}><span>{String(receipt.sequence).padStart(2, '0')}</span><div><b>{receipt.kind}</b><small>{new Date(Number(receipt.observedAtUnixMs)).toLocaleString('zh-CN')} · {receipt.eventHash.slice(0, 12)}</small></div></article>) : <div className="empty-state"> PNCC </div>}</section>
</div>
</section>
)
const renderSystem = () => (
<section className="content-page">
<header className="page-title"><div><span className="kicker">SYSTEM EVIDENCE</span><h1></h1><p></p></div></header>
<div className="system-grid">
<section className="plain-panel connection-panel">
<header><div><h2></h2><p>Unix Stream MCP </p></div><span className={connectionOnline ? 'status-chip online' : 'status-chip'}>{connectionOnline ? `${status.directConnectionCount} 个客户端在线` : '当前无客户端'}</span></header>
<dl className="evidence-list"><div><dt></dt><dd>{status.directLocalBrokerState}</dd></div><div><dt></dt><dd>{status.directConnectionCount}</dd></div><div><dt></dt><dd>{status.resumableSessionCount}</dd></div><div><dt></dt><dd>UNIX_STREAM_JSON_LINES</dd></div><div><dt>MCP </dt><dd> / / </dd></div></dl>
{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></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>
{systemMessage && <p className="global-message">{systemMessage}</p>}
</section>
)
return <div className="app-shell">
<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>
</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>
</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>
}
createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp/></StrictMode>)