hololake-system-architecture/product-source/guanghu-education-industry-os/src/App.tsx

357 lines
16 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 { invoke } from '@tauri-apps/api/core'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { useMemo, useState, type MouseEvent } from 'react'
import {
bundledModules,
formatForgejoVersion,
parseSheetMetrics,
routeInstruction,
type InstructionRoute,
type ModuleEntry,
type Surface,
} from './core/guanghuKernel'
type JsonRecord = Record<string, unknown>
type ChannelSnapshot = {
node: JsonRecord
fifthDomainLink: JsonRecord
moduleRegistry: { modules?: ModuleEntry[] }
updateAuthority: JsonRecord
channel: JsonRecord & { mountedModules?: string[] }
codeChannel: {
version?: { version?: string }
repositories?: { data?: unknown[] }
}
}
type SaveReceipt = {
channelId: string
moduleId: string
sha256: string
writtenAt: string
state: string
}
const defaultDocument = `# 高中项目师训工作三年专项规划2026—2029
## 第一阶段:攻坚筑基
- 建立全职导师试点和导师工时保障机制
- 形成新师培训、质控、留存三条可量化工作线
- 将责任人、时间节点和复盘回执写入同一个工作频道
## 第二阶段:提质增效
- 双轨培养与分层培训
- 教研训一体化
- 质控联动闭环与预警
## 第三阶段:深耕塑牌
- 沉淀可复制的高中项目师训模块
- 形成跨项目可装配的教育行业能力包
`
const defaultSheet = `教师,培养进度
孙雨桐,72
周子航,65
吴佳怡,58
郑思远,42
林晓彤,38`
const navigation: Array<{ id: Surface; label: string; mark: string }> = [
{ id: 'channel', label: '我的频道', mark: '湖' },
{ id: 'document', label: '智能文档', mark: '文' },
{ id: 'sheet', label: '智能表格', mark: '表' },
{ id: 'dashboard', label: '图表与仪表盘', mark: '图' },
{ id: 'modules', label: '教育模块库', mark: '件' },
{ id: 'brain', label: '人格体脑区', mark: '脑' },
{ id: 'receipts', label: '执行回执', mark: '证' },
]
function LakeMark() {
return <span className="lake-mark" aria-hidden="true"><i /><b /></span>
}
function WindowDragHandle() {
const startDragging = (event: MouseEvent<HTMLDivElement>) => {
if (event.button !== 0) return
event.preventDefault()
void getCurrentWindow().startDragging()
}
return <div
className="native-drag-handle"
data-tauri-drag-region
aria-label="拖动窗口"
onMouseDown={startDragging}
/>
}
function LoginGate({
error,
loading,
onConnect,
}: {
error: string
loading: boolean
onConnect: () => void
}) {
return <main className="login-gate">
<WindowDragHandle />
<section className="login-window">
<LakeMark />
<p> · </p>
<h1></h1>
<span></span>
<button className="server-entry" disabled={loading} onClick={onConnect}>
<i />
<span><strong>{loading ? '正在连接小新服务器…' : '进入小新 · 高中项目师训'}</strong><small>XX-GZ-001 · </small></span>
<b></b>
</button>
{error && <div className="login-error">{error}</div>}
<div className="reserved-logins">
<span></span>
<button disabled></button><button disabled></button>
<button disabled></button><button disabled></button>
</div>
<footer><i />线 · </footer>
</section>
</main>
}
function StatusCard({ label, value, detail, tone = 'green' }: {
label: string
value: string
detail: string
tone?: 'green' | 'blue' | 'amber'
}) {
return <article className={`status-card status-card--${tone}`}>
<span>{label}</span><strong>{value}</strong><small>{detail}</small>
</article>
}
function ChannelHome({ snapshot, modules }: { snapshot: ChannelSnapshot; modules: ModuleEntry[] }) {
const live = modules.filter((module) => module.state === 'LIVE_STAGE_1').length
const brainCandidates = modules.filter((module) => module.group === 'persona-brain').length
const liveAgent = modules.some((module) => module.group === 'persona-brain' && module.state === 'LIVE_STAGE_1')
const mounted = snapshot.channel.mountedModules?.length ?? 0
const repositoryCount = snapshot.codeChannel.repositories?.data?.length ?? 0
return <div className="page-stack">
<header className="page-intro">
<p>GH-EDU-CHANNEL-XX-001</p>
<h2> · </h2>
<span></span>
</header>
<section className="status-grid">
<StatusCard label="个人服务器" value={String(snapshot.node.state ?? '已连接')} detail="身份与现实数据锚点 · XX-GZ-001" />
<StatusCard label="第五域协议" value={String(snapshot.fifthDomainLink.state ?? '未知')} detail="上游导航 · JD-FD-PRIMARY" tone="blue" />
<StatusCard label="真实能力" value={`${live}`} detail={`模板已装配 ${mounted} 个模块地址`} tone="amber" />
<StatusCard label="代码频道" value={formatForgejoVersion(snapshot.codeChannel.version?.version)} detail={`${repositoryCount} 个服务器内仓库 · Forgejo 数据面`} tone="blue" />
<StatusCard label="人格体 Agent" value={liveAgent ? '已接入' : '未接入'} detail={`仅登记 ${brainCandidates} 个研究候选`} tone="amber" />
</section>
<section className="work-grid">
<article className="work-panel">
<header><h3></h3><span></span></header>
<ul>
<li><b>01</b><span><strong></strong><small>线</small></span></li>
<li><b>02</b><span><strong></strong><small></small></span></li>
<li><b>03</b><span><strong></strong><small></small></span></li>
</ul>
</article>
<article className="work-panel">
<header><h3></h3><span></span></header>
<div className="rule-list">
<p><i /></p>
<p><i /></p>
<p><i /></p>
<p><i />使</p>
</div>
</article>
</section>
</div>
}
function ServerEditor({
kind,
value,
onChange,
onSave,
saving,
}: {
kind: 'document' | 'sheet'
value: string
onChange: (value: string) => void
onSave: () => void
saving: boolean
}) {
return <section className="editor-page">
<header>
<div><p>{kind === 'document' ? 'GH-EDU-OFFICE-DOC-001' : 'GH-EDU-OFFICE-SHEET-001'}</p>
<h2>{kind === 'document' ? '智能文档' : '智能表格数据'}</h2>
<span></span></div>
<button onClick={onSave} disabled={saving}>{saving ? '正在写入服务器…' : '保存并生成回执'}</button>
</header>
<textarea aria-label={kind === 'document' ? '服务器文档' : '服务器表格'} value={value} onChange={(event) => onChange(event.target.value)} />
</section>
}
function Dashboard({ sheet }: { sheet: string }) {
const metrics = parseSheetMetrics(sheet)
const highest = metrics.reduce((best, item) => item.value > best.value ? item : best, { label: '—', value: 0 })
const average = metrics.length ? Math.round(metrics.reduce((sum, item) => sum + item.value, 0) / metrics.length * 10) / 10 : 0
return <div className="page-stack">
<header className="page-intro"><p>GH-EDU-OFFICE-DASH-001</p><h2></h2><span></span></header>
<section className="status-grid">
<StatusCard label="新师数量" value={`${metrics.length}`} detail="当前表格有效记录" />
<StatusCard label="平均进度" value={`${average}%`} detail="按当前表格计算" tone="blue" />
<StatusCard label="最高进度" value={highest.label} detail={`${highest.value}%`} tone="amber" />
</section>
<section className="chart-panel">
{metrics.map((item) => <div className="chart-row" key={item.label}>
<span>{item.label}</span><div><i style={{ width: `${Math.max(3, item.value)}%` }} /></div><strong>{item.value}%</strong>
</div>)}
</section>
</div>
}
function ModuleLibrary({ modules }: { modules: ModuleEntry[] }) {
const groups = [
['foundation', '系统地基'], ['office', '办公通用'], ['education', '教育通用'],
['xiaoxin-template', '小新师训模板'], ['persona-brain', '人格体脑区'],
]
return <div className="page-stack">
<header className="page-intro"><p>GH-EDU-MODULE-REGISTRY-0001</p><h2></h2><span>{modules.length} </span></header>
{groups.map(([id, label]) => <section className="module-group" key={id}>
<header><h3>{label}</h3><span>{modules.filter((module) => module.group === id).length} </span></header>
<div>{modules.filter((module) => module.group === id).map((module) =>
<article key={module.id}><code>{module.id}</code><strong>{module.title}</strong>
<span className={`module-state module-state--${module.state.toLowerCase()}`}>{module.state}</span>
<small>{module.capabilities.join(' · ') || module.source}</small></article>)}</div>
</section>)}
</div>
}
function BrainLibrary({ modules }: { modules: ModuleEntry[] }) {
const brain = modules.filter((module) => module.group === 'persona-brain')
return <div className="page-stack">
<header className="page-intro"><p>GH-EDU-PERSONA-BRAIN-CANDIDATES</p><h2></h2>
<span> Agent </span></header>
<section className="brain-grid">{brain.map((module, index) =>
<article key={module.id}><b>{String(index + 1).padStart(2, '0')}</b><div><code>{module.id}</code><strong>{module.title}</strong><small>{module.source}</small></div><span></span></article>)}</section>
</div>
}
function Receipts({ receipts, snapshot }: { receipts: SaveReceipt[]; snapshot: ChannelSnapshot }) {
return <div className="page-stack">
<header className="page-intro"><p>GH-EDU-CORE-RECEIPT-001</p><h2></h2><span></span></header>
<section className="receipt-list">
<article><i /><div><strong></strong><code>{String(snapshot.fifthDomainLink.state ?? 'UNKNOWN')}</code><small>{String(snapshot.fifthDomainLink.observed_at ?? '')}</small></div></article>
{receipts.map((receipt) => <article key={`${receipt.moduleId}-${receipt.writtenAt}`}><i /><div>
<strong>{receipt.moduleId}</strong><code>{receipt.state}</code><small>{receipt.sha256} · {receipt.writtenAt}</small>
</div></article>)}
{receipts.length === 0 && <p></p>}
</section>
</div>
}
function App() {
const [snapshot, setSnapshot] = useState<ChannelSnapshot | null>(null)
const [surface, setSurface] = useState<Surface>('channel')
const [instruction, setInstruction] = useState('')
const [route, setRoute] = useState<InstructionRoute | null>(null)
const [document, setDocument] = useState(defaultDocument)
const [sheet, setSheet] = useState(defaultSheet)
const [receipts, setReceipts] = useState<SaveReceipt[]>([])
const [connecting, setConnecting] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const modules = useMemo(
() => snapshot?.moduleRegistry.modules ?? bundledModules(),
[snapshot],
)
const connect = async () => {
setConnecting(true)
setError('')
try {
const [nextSnapshot, remoteDocument, remoteSheet] = await Promise.all([
invoke<ChannelSnapshot>('connect_channel'),
invoke<string>('read_channel_file', { kind: 'document' }),
invoke<string>('read_channel_file', { kind: 'sheet' }),
])
setSnapshot(nextSnapshot)
if (remoteDocument.trim()) setDocument(remoteDocument)
if (remoteSheet.trim()) setSheet(remoteSheet)
} catch (reason) {
setError(`服务器入口未通过:${String(reason)}`)
} finally {
setConnecting(false)
}
}
const save = async (kind: 'document' | 'sheet') => {
setSaving(true)
setError('')
try {
const receipt = await invoke<SaveReceipt>('save_channel_file', {
kind,
content: kind === 'document' ? document : sheet,
})
setReceipts((current) => [receipt, ...current.filter((item) => item.moduleId !== receipt.moduleId)])
setRoute({
moduleId: receipt.moduleId,
surface: kind,
intent: '保存到个人服务器',
state: 'LIVE_STAGE_1',
at: receipt.writtenAt,
})
} catch (reason) {
setError(`服务器写入失败:${String(reason)}`)
} finally {
setSaving(false)
}
}
const runInstruction = () => {
const next = routeInstruction(instruction, modules)
setRoute(next)
setSurface(next.surface)
}
if (!snapshot) return <LoginGate error={error} loading={connecting} onConnect={connect} />
return <main className="education-os">
<WindowDragHandle />
<aside className="side-rail">
<div className="brand"><LakeMark /><span><strong> · </strong><small>EDUCATION SUBDOMAIN OS</small></span></div>
<nav>{navigation.map((item) => <button key={item.id} className={surface === item.id ? 'active' : ''} onClick={() => setSurface(item.id)}>
<i>{item.mark}</i><span>{item.label}</span></button>)}</nav>
<footer><span><i />XX-GZ-001 </span><small></small></footer>
</aside>
<section className="main-workspace">
<header className="topbar" data-tauri-drag-region>
<div data-tauri-drag-region><small></small><strong> · </strong></div>
<div className="command-box">
<input value={instruction} onChange={(event) => setInstruction(event.target.value)} onKeyDown={(event) => event.key === 'Enter' && runInstruction()} placeholder="说出要打开的真实能力,例如:整理三年规划文档" />
<button onClick={runInstruction}></button>
</div>
<div className="node-pill"><i />线</div>
</header>
{route && <div className="route-strip"><span></span><code>{route.moduleId}</code><b>{route.state}</b></div>}
{error && <div className="workspace-error">{error}</div>}
<div className="page">
{surface === 'channel' && <ChannelHome snapshot={snapshot} modules={modules} />}
{surface === 'document' && <ServerEditor kind="document" value={document} onChange={setDocument} onSave={() => save('document')} saving={saving} />}
{surface === 'sheet' && <ServerEditor kind="sheet" value={sheet} onChange={setSheet} onSave={() => save('sheet')} saving={saving} />}
{surface === 'dashboard' && <Dashboard sheet={sheet} />}
{surface === 'modules' && <ModuleLibrary modules={modules} />}
{surface === 'brain' && <BrainLibrary modules={modules} />}
{surface === 'receipts' && <Receipts receipts={receipts} snapshot={snapshot} />}
</div>
</section>
</main>
}
export default App