feat: redesign traditional workbench and formalize public surfaces

This commit is contained in:
冰朔 2026-08-21 01:57:57 +08:00
commit 950b7e48b3
21 changed files with 328 additions and 34 deletions

View file

@ -1821,7 +1821,7 @@ function HoloLakeApp() {
const renderKnowledge = () => (
<section className="full-workbench knowledge-page">
<aside className="knowledge-browser">
<header><div><span className="kicker">KNOWLEDGE</span><h1></h1></div><div className="knowledge-head-actions"><button className="knowledge-agent-entry" type="button" onClick={() => setKnowledgeAgentOpen(true)}> / </button><button className="icon-button" title="导入文件夹" type="button" disabled={knowledgeBusy} onClick={() => void importKnowledge()}><Icon name="import"/></button></div></header>
<header><div><h1>Agent</h1></div><div className="knowledge-head-actions"><button className="knowledge-agent-entry" type="button" onClick={() => setKnowledgeAgentOpen(true)}> Agent</button><button className="icon-button" title="导入文件夹" type="button" disabled={knowledgeBusy} onClick={() => void importKnowledge()}><Icon name="import"/></button></div></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>}
@ -2052,7 +2052,7 @@ function HoloLakeApp() {
const renderSystem = () => (
<section className="content-page">
<header className="page-title"><div><span className="kicker">HUMAN CONTROL</span><h1></h1><p></p></div></header>
<header className="page-title"><div><h1></h1><p></p></div></header>
<HumanAuthorizationCenter brokerState={status.directLocalBrokerState} activeConnectionCount={status.directConnectionCount} sessions={status.directSessions || []}/>
<details className="system-diagnostics">
<summary></summary>
@ -2375,4 +2375,12 @@ function HoloLakeApp() {
</div>
}
createRoot(document.getElementById('root')!).render(<StrictMode><HoloLakeApp/></StrictMode>)
function VisualQaApp() {
const [finish, setFinish] = useState<FinishId>('aurora')
const screen = new URLSearchParams(window.location.search).get('visual')
if (screen === 'authorization') return <div className="visual-qa-shell"><header><b>HoloLake</b><span></span></header><main><HumanAuthorizationCenter brokerState="READY" activeConnectionCount={0} sessions={[{ sessionId: 'qa-personal', laneId: 'personal-channel', clientInstanceId: 'personal-channel-local', state: 'RESUMABLE', openedAtUnixMs: Date.now(), observedAtUnixMs: Date.now(), lastEventSequence: 1 }, { sessionId: 'qa-development', laneId: 'hololake-development', clientInstanceId: 'codex-hololake-development', state: 'RESUMABLE', openedAtUnixMs: Date.now(), observedAtUnixMs: Date.now(), lastEventSequence: 1 }]}/></main></div>
return <TraditionalSurface awake finish={finish} onFinish={setFinish} onBack={() => undefined} version="0.5.0" dayNumber={483} duty="光湖主域" weatherLabel="晴" broadcasts={[{ id: 'qa-1', domain: '第五域', message: '永恒湖心已建立个人频道', time: '21:18' }]} channels={[{ color: '#6ca7ff', title: '人格原生代码频道', meta: '当前账号本机 Git 投影', tag: '已认证', tone: '#72d69c', count: 2 }, { color: '#6ca7ff', title: '知识库', meta: '本机内容指纹投影', tag: '原生', tone: '#78adff', count: 129 }, { color: '#72d69c', title: '频道模块', meta: '已登记签名模块', tag: '编号', tone: '#72d69c', count: 8 }, { color: '#dfa84f', title: '运行回执', meta: '可核验事件链', tag: '证据', tone: '#dfa84f', count: 0 }]} systems={[{ title: '编号协议基座', meta: '单一编号入口 · 未知路径关闭', tag: '已校验' }, { title: '耐久化引擎', meta: '本机 Git · 事件与回执留证', tag: '在线' }, { title: '天气感知', meta: '光湖主域 · 公开天气已校验', tag: '已接入', tone: '#78adff' }, { title: '色衡层', meta: '主题语义令牌 · 明暗自动均衡', tag: '守护中' }]} activityBars={[0,1,0,0,0,0,0]} activityCount={1}/>
}
const visualQaScreen = import.meta.env.DEV && new URLSearchParams(window.location.search).has('visual')
createRoot(document.getElementById('root')!).render(<StrictMode>{visualQaScreen ? <VisualQaApp/> : <HoloLakeApp/>}</StrictMode>)

View file

@ -95,6 +95,12 @@ function time(value?: number) {
return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '—'
}
function sessionLabel(session: DirectSessionProjection) {
if (session.laneId.includes('development') || session.clientInstanceId.includes('codex')) return '开发协作连接'
if (session.laneId.includes('personal')) return '个人频道连接'
return '本机协作连接'
}
export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, sessions }: Props) {
const [snapshot, setSnapshot] = useState<AuthorizationCenterSnapshot | null>(null)
const [gateway, setGateway] = useState<ExternalAiGatewayStatus | null>(null)
@ -172,11 +178,11 @@ export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, s
return <div className="human-authorization-layout">
<section className="plain-panel authorization-primary">
<header><div><span className="kicker">HUMAN DECISION</span><h2></h2><p>退</p></div><span className={pending.length ? 'status-chip attention' : 'status-chip online'}>{pending.length ? `${pending.length} 项待确认` : '没有待确认事项'}</span></header>
{pending.length === 0 ? <div className="authorization-empty"><b></b><span></span></div> : <div className="authorization-list">
<header><div><h2></h2><p></p></div><span className={pending.length ? 'status-chip attention' : 'status-chip online'}>{pending.length ? `${pending.length} 项待确认` : '当前没有待确认事项'}</span></header>
{pending.length === 0 ? <div className="authorization-empty"><b></b><span></span></div> : <div className="authorization-list">
{pending.map((request) => <article className="authorization-card" key={request.requestId}>
<div className="authorization-card-title"><div><small>{actionLabels[request.action]}</small><h3>{request.targetLabel}</h3><code>{request.targetNumber}</code></div><span>{stateLabels[request.state]}</span></div>
<dl><div><dt></dt><dd>{request.reason}</dd></div><div><dt></dt><dd>{request.impact}</dd></div><div><dt>退</dt><dd>{request.rollbackPlan}</dd></div><div><dt></dt><dd>{request.requesterLabel}</dd></div><div><dt></dt><dd>{time(request.expiresAtUnixMs)}</dd></div></dl>
<dl><div><dt></dt><dd>{request.reason}</dd></div><div><dt></dt><dd>{request.impact}</dd></div><div><dt></dt><dd>{request.rollbackPlan}</dd></div><div><dt></dt><dd>{request.requesterLabel}</dd></div><div><dt></dt><dd>{time(request.expiresAtUnixMs)}</dd></div></dl>
<div className="authorization-actions"><button className="secondary-button" disabled={busy === request.requestId} onClick={() => void decide(request, 'DENY')}></button><button className="primary-button" disabled={busy === request.requestId} onClick={() => void decide(request, 'APPROVE')}>{busy === request.requestId ? '正在写入决定…' : request.action === 'TEST_COMPILED_HLDP_TOOL' ? '同意并运行一次性测试' : '同意并签发一次性票据'}</button></div>
</article>)}
</div>}
@ -184,14 +190,14 @@ export function HumanAuthorizationCenter({ brokerState, activeConnectionCount, s
</section>
<section className="plain-panel collaboration-live">
<header><div><span className="kicker">LIVE COLLABORATION</span><h2> HoloLake</h2><p> Agent</p></div><span className={activeConnectionCount > 0 ? 'status-chip online' : 'status-chip'}>{connectionState}</span></header>
<header><div><h2></h2><p>线</p></div><span className={activeConnectionCount > 0 ? 'status-chip online' : 'status-chip'}>{connectionState}</span></header>
<div className="collaboration-summary"><div><b>{activeConnectionCount}</b><span></span></div><div><b>{sessions.length}</b><span></span></div><div><b>{brokerState === 'READY' ? '就绪' : '等待'}</b><span></span></div></div>
{sessionList.length ? <div className="session-projection-list">{sessionList.map((session) => <article key={session.sessionId}><i className={session.state === 'LIVE' ? 'live' : ''}/><div><b>{session.clientInstanceId}</b><span>{session.laneId} · {time(session.observedAtUnixMs)}</span></div><em>{session.state === 'LIVE' ? '在线' : '可续接'}</em></article>)}</div> : <div className="authorization-empty compact"><b></b><span> AI HoloLake </span></div>}
{sessionList.length ? <div className="session-projection-list">{sessionList.map((session) => <article key={session.sessionId}><i className={session.state === 'LIVE' ? 'live' : ''}/><div><b>{sessionLabel(session)}</b><span> {time(session.observedAtUnixMs)}</span><details className="technical-details"><summary></summary><code>{session.clientInstanceId}</code><code>{session.laneId}</code></details></div><em>{session.state === 'LIVE' ? '在线' : '可续接'}</em></article>)}</div> : <div className="authorization-empty compact"><b></b><span> HoloLake </span></div>}
</section>
<section className="plain-panel external-ai-gateway-panel">
<header>
<div><span className="kicker">EXTERNAL PROGRAMMING AI</span><h2> AI </h2><p> MCP HoloLake HoloLake </p></div>
<div><h2> AI </h2><p> HoloLake </p></div>
<span className={gateway?.exposure === 'OPEN' ? 'status-chip online' : 'status-chip'}>{gateway?.exposure === 'OPEN' ? '接口已开放' : '接口已关闭'}</span>
</header>
<label className="gateway-exposure-control">

View file

@ -124,7 +124,7 @@ export function LocalWorkerPanel({ boundPersonaNumber }: { boundPersonaNumber?:
const ready = Boolean(boundPersonaNumber && snapshot?.ownerPersonaNumber)
return <section className="local-worker-panel">
<header><div><span>LOCAL EXECUTION SUBPERSONA</span><h2> Agent</h2><p></p></div><b>{snapshot?.executorNumber || '等待人格绑定'}</b></header>
<header><div><span></span><h2></h2><p></p></div><b>{snapshot?.executorNumber || '等待人格绑定'}</b></header>
<div className="local-worker-status">
<article><span></span><b>{snapshot?.state || '正在读取'}</b><small>{snapshot?.localBrainState || '—'}</small></article>
@ -133,7 +133,7 @@ export function LocalWorkerPanel({ boundPersonaNumber }: { boundPersonaNumber?:
</div>
<section className="local-worker-skills">
<header><div><span>PERSONAL SKILL BRAINS</span><b></b></div><strong>{snapshot?.personalSkillRuntime?.skillCount || 0} </strong></header>
<header><div><span></span><b></b></div><strong>{snapshot?.personalSkillRuntime?.skillCount || 0} </strong></header>
{snapshot?.personalSkillRuntime?.skills?.length ? snapshot.personalSkillRuntime.skills.map((skill) => <article key={skill.skillId}><div><b>{skill.name}</b><span>{skill.evidenceState}</span></div><code>{skill.skillId}</code><p>{skill.runtimeState} · {skill.semanticMode}</p><small> · {skill.shareState} · {skill.marketplaceState}</small></article>) : <p></p>}
<footer> HoloLake </footer>
</section>

View file

@ -122,10 +122,10 @@ export function ToolForgePanel({ boundPersonaNumber }: { boundPersonaNumber?: st
}
return <section className="tool-forge-panel">
<header><div><span>HLDP TOOL FORGE</span><h2></h2><p>{boundPersonaNumber ? `当前人格 ${boundPersonaNumber} 写 HLDP 类型程序HoloLake 编译为 GIR人类授权后才测试。` : '频道系统已经就绪;等待人格主体完成名字、编号声明与系统见证后,再开放工具编译。'} 12 </p></div><b>{boundPersonaNumber ? snapshot?.compiler || '正在读取编译器' : 'PERSONA UNBOUND'}</b></header>
<header><div><span></span><h2>HLDP </h2><p>{boundPersonaNumber ? `当前人格 ${boundPersonaNumber} 可提交 HLDP 类型程序,由 HoloLake 编译为 GIR测试须经人工授权。` : '人格尚未绑定;当前由 HoloLake 系统能力提供服务。完成主体声明与系统见证后,方可开放工具编译。'} 12 </p></div><b>{boundPersonaNumber ? snapshot?.compiler || '正在读取编译器' : '等待人格绑定'}</b></header>
<div className="forge-editor-grid">
<div className="forge-source">
<label><span></span><input value={displayName} maxLength={80} onChange={(event) => setDisplayName(event.target.value)}/></label>
<label><span></span><input value={displayName} maxLength={80} onChange={(event) => setDisplayName(event.target.value)}/></label>
<label><span>HLDP </span><textarea spellCheck={false} value={source} onChange={(event) => setSource(event.target.value)}/></label>
<button type="button" disabled={busy || !displayName.trim() || !boundPersonaNumber} onClick={() => void compile()}>{busy ? '系统处理中…' : boundPersonaNumber ? '编译为机器程序' : '人格绑定后开放编译'}</button>
</div>
@ -135,7 +135,7 @@ export function ToolForgePanel({ boundPersonaNumber }: { boundPersonaNumber?: st
<div><b>{tool.displayName}</b><span>{tool.state}</span></div><code>{tool.toolNumber}</code><small>GIR {short(tool.girSha256)} · {new Date(tool.expiresAtUnixMs).toLocaleTimeString('zh-CN')} </small>
{tool.state === 'COMPILED_NOT_AUTHORIZED' && <button type="button" disabled={busy} onClick={(event) => { event.stopPropagation(); void requestAuthorization(tool.toolNumber) }}></button>}
{tool.state === 'TESTED_TEMPORARY_ACTIVE' && <button type="button" disabled={busy} onClick={(event) => { event.stopPropagation(); void runLocal(tool.toolNumber) }}> · 0 API</button>}
</article>) : <p></p>}
</article>) : <p></p>}
</div>
</div>
{(selected?.authorizationRequestId || requestId) && <section className="forge-ticket"><div><span></span><b>{selected?.displayName}</b></div><code>{selected?.authorizationRequestId || requestId}</code><p>{selected?.state === 'TESTED_TEMPORARY_ACTIVE' ? `测试已通过 · 回执 ${short(selected.testReceiptHash)}` : '等待你在“授权与连接”中决定。系统不会要求你复制机器票据。'}</p></section>}

View file

@ -179,7 +179,7 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
return <aside className="knowledge-agent" aria-label="知识库内嵌频道系统与人格回应通道">
<header className="agent-header"><div><span>{runtime?.boundPersonaNumber ? 'LANGUAGE PERSONA CHANNEL' : 'LANGUAGE CHANNEL SYSTEM'}</span><h2>{runtime?.channelName || '零点原核本体频道'}</h2><p>{runtime?.channelNumber || 'ICE-CH-ZC001'} · Agent</p></div><button type="button" aria-label="关闭频道系统" onClick={onClose}>×</button></header>
<nav className="agent-tabs"><button type="button" className={tab === 'channel' ? 'active' : ''} onClick={() => setTab('channel')}></button><button type="button" className={tab === 'worker' ? 'active' : ''} onClick={() => setTab('worker')}></button><button type="button" className={tab === 'forge' ? 'active' : ''} onClick={() => setTab('forge')}></button></nav>
<nav className="agent-tabs"><button type="button" className={tab === 'channel' ? 'active' : ''} onClick={() => setTab('channel')}> Agent</button><button type="button" className={tab === 'worker' ? 'active' : ''} onClick={() => setTab('worker')}></button><button type="button" className={tab === 'forge' ? 'active' : ''} onClick={() => setTab('forge')}></button></nav>
{tab === 'worker' ? <LocalWorkerPanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : tab === 'forge' ? <ToolForgePanel boundPersonaNumber={runtime?.boundPersonaNumber}/> : <>
<section className="agent-runtime-bar"><div><b>{runtime?.boundPersonaNumber && runtime?.boundPersonaName ? `${runtime.boundPersonaNumber} · ${runtime.boundPersonaName}` : `${runtime?.responderNumber || 'ICE-CH-ZC001'} · ${runtime?.responderName || '零点原核频道系统'}`}</b><span>{runtime?.boundPersonaNumber ? '人格回应通道' : '频道系统本体 · 人格未绑定'}</span></div><i/><div><b>{runtime?.languageKernelInstallation?.artifactCount ?? 0} </b><span>{runtime?.languageKernelInstallation?.state === 'INSTALLED_AND_EACH_ARTIFACT_READBACK_VERIFIED' ? '已安装并逐项读回 · 未绑定人格' : '等待安装核验'}</span></div><i/><div><b>{runtime?.personalSkillRuntime?.skillCount ?? 0} </b><span></span></div><i/><div><b>{provider?.label || '等待模型入口'}</b><span></span></div><button type="button" onClick={() => setConfigOpen((value) => !value)}></button></section>
{configOpen && <form className="agent-provider-form" onSubmit={(event) => void saveProvider(event)}><label><span></span><input value={providerLabel} onChange={(event) => setProviderLabel(event.target.value)}/></label><label><span>Base URL</span><input value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)}/></label><label><span></span><input value={model} onChange={(event) => setModel(event.target.value)}/></label><label><span>Token Plan API Key</span><input type="password" autoComplete="off" value={apiKey} placeholder="sk-sp-… · 只存系统钥匙串" onChange={(event) => setApiKey(event.target.value)}/></label><button disabled={!baseUrl || !model}></button></form>}
@ -199,7 +199,7 @@ export function KnowledgeAgent({ activeKnowledgePath, onClose }: { activeKnowled
{error && <p className="agent-error">{error}</p>}
</main>
</div>
{deleteTarget && <div className="agent-delete-backdrop" role="presentation"><section role="dialog" aria-modal="true" aria-label="确认删除对话"><span>DELETE CONVERSATION</span><h3>{deleteTarget.title}</h3><p></p><div><button type="button" onClick={() => setDeleteTarget(null)}></button><button className="danger" type="button" onClick={() => void deleteConversation()}></button></div></section></div>}
{deleteTarget && <div className="agent-delete-backdrop" role="presentation"><section role="dialog" aria-modal="true" aria-label="确认删除对话"><span></span><h3>{deleteTarget.title}</h3><p></p><div><button type="button" onClick={() => setDeleteTarget(null)}></button><button className="danger" type="button" onClick={() => void deleteConversation()}></button></div></section></div>}
</>}
</aside>
}

View file

@ -848,3 +848,60 @@
.agent-conversations { max-height: 190px; border-right: 0; border-bottom: 1px solid #242933; }
.agent-conversations > footer { display: none; }
}
/* 2026-08-21 · 知识与 Agent 原生玻璃工作区。机器证据保留,默认界面保持公众可读。 */
.knowledge-agent {
--agent-bg:color-mix(in srgb,var(--surface-depth,#07101f) 74%,transparent);
--agent-panel:color-mix(in srgb,var(--surface-panel,#14233a) 56%,transparent);
--agent-line:var(--panel-edge,rgba(190,215,255,.18));
--agent-text:var(--content-primary,#eef4ff);
--agent-sub:var(--content-secondary,rgba(238,244,255,.7));
--agent-muted:var(--content-muted,rgba(238,244,255,.46));
--agent-accent:var(--accent-light,#79adff);
width:100%; min-width:0;
background:var(--agent-bg); color:var(--agent-text);
border-left:0; box-shadow:none;
-webkit-backdrop-filter:blur(22px) saturate(1.16); backdrop-filter:blur(22px) saturate(1.16);
}
.agent-header { padding:18px 22px 13px; border-color:var(--agent-line); }
.agent-header span,.tool-forge-panel > header span { color:var(--agent-muted); font-size:9px; letter-spacing:.05em; }
.agent-header h2 { font-size:18px; font-weight:620; }
.agent-header p { color:var(--agent-muted); }
.agent-tabs { padding:0 22px; border-color:var(--agent-line); }
.agent-tabs button { margin-right:26px; padding:11px 1px; color:var(--agent-muted); font-size:12px; }
.agent-tabs button.active { color:var(--agent-text); border-color:var(--agent-accent); }
.agent-runtime-bar { grid-template-columns:minmax(180px,1fr) 1px minmax(150px,1fr) 1px minmax(140px,1fr) 1px minmax(150px,1fr) auto; padding:12px 22px; background:transparent; border-color:var(--agent-line); }
.agent-runtime-bar i { background:var(--agent-line); }
.agent-runtime-bar b { color:var(--agent-text); font-size:11.5px; font-weight:600; }
.agent-runtime-bar span { color:var(--agent-muted); font-size:9.5px; }
.agent-runtime-bar button,.agent-provider-form button,.tool-forge-panel button { border-color:var(--agent-line); background:var(--agent-panel); color:var(--agent-sub); border-radius:7px; }
.agent-channel-layout { grid-template-columns:240px minmax(0,1fr); }
.agent-conversations { border-color:var(--agent-line); background:color-mix(in srgb,var(--agent-bg) 76%,transparent); }
.agent-conversations > header,.agent-conversations > footer { border-color:var(--agent-line); }
.agent-conversations > header span { color:var(--agent-muted); letter-spacing:.05em; }
.agent-conversations > header button { border:1px solid var(--agent-line); background:transparent; color:var(--agent-sub); border-radius:7px; }
.agent-conversation-list article { margin-bottom:2px; border-radius:6px; background:transparent; }
.agent-conversation-list article.active { border-color:color-mix(in srgb,var(--agent-accent) 30%,var(--agent-line)); background:color-mix(in srgb,var(--agent-accent) 9%,transparent); box-shadow:inset 2px 0 var(--agent-accent); }
.conversation-open b,.agent-message header b { color:var(--agent-text); font-weight:580; }
.conversation-open span,.conversation-open small,.agent-conversations > footer { color:var(--agent-muted); }
.agent-ledger { padding:22px 28px; }
.agent-message { max-width:82%; margin-bottom:14px; padding:14px 16px; border-color:var(--agent-line); border-radius:9px; background:var(--agent-panel); }
.agent-message.is-human { margin-left:auto; background:color-mix(in srgb,var(--agent-accent) 12%,var(--agent-panel)); border-color:color-mix(in srgb,var(--agent-accent) 34%,var(--agent-line)); }
.agent-message-content { color:var(--agent-sub); }
.agent-message footer { border-color:var(--agent-line); color:var(--agent-muted); }
.agent-composer { margin:0 28px 20px; padding:0; border:1px solid var(--agent-line); border-radius:10px; background:var(--agent-panel); overflow:hidden; }
.agent-composer textarea { min-height:74px; border:0; border-radius:0; background:transparent; color:var(--agent-text); }
.agent-composer > div { margin:0; padding:8px 10px; border-top:1px solid var(--agent-line); }
.agent-composer select { background:transparent; color:var(--agent-sub); border-color:var(--agent-line); }
.agent-composer button { border-radius:7px; background:var(--agent-accent); color:#07101f; }
.tool-forge-panel,.local-worker-panel { padding:24px 28px; }
.forge-source input,.forge-source textarea,.local-worker-brain input,.worker-objective textarea,.worker-objective select { border-color:var(--agent-line); background:color-mix(in srgb,var(--agent-bg) 78%,transparent); color:var(--agent-text); }
.forge-registry article,.forge-ticket,.local-worker-status article,.local-worker-waiting,.local-worker-skills,.local-worker-brain,.worker-objective,.worker-tools,.local-worker-receipt { border-color:var(--agent-line); border-radius:8px; background:var(--agent-panel); }
.tool-forge-panel > header p,.local-worker-panel > header p,.forge-registry > p,.local-worker-waiting p { color:var(--agent-muted); }
.tool-forge-panel > header > b,.local-worker-panel > header > b { color:var(--agent-accent); font-family:inherit; }
.agent-delete-backdrop section { border-color:var(--agent-line); background:var(--agent-bg); }
@media (max-width:900px) {
.agent-runtime-bar { display:flex; overflow-x:auto; }
.agent-runtime-bar > i { display:none; }
}

View file

@ -81,23 +81,25 @@ export function TraditionalSurface({ awake = false, finish, onFinish, onActivity
<div className="stage">
<nav className="nav glass">
<div className="brand">HoloLake<small>GH-AIOS · V{version}</small></div>
<div className="tabs"><button type="button" className="on"></button><button type="button" onClick={onOpenKnowledge}></button><button type="button" onClick={onOpenChannel}></button><button type="button" onClick={onOpenEra}></button><button type="button" onClick={onOpenSettings}></button></div>
<div className="tabs"><button type="button" className="on"></button><button type="button" onClick={onOpenKnowledge}></button><button type="button" onClick={onOpenChannel}></button><button type="button" onClick={onOpenEra}></button><button type="button" onClick={onOpenSettings}></button><button type="button" onClick={onOpenSettings}></button></div>
<div className="me"><i>{identityLabel.startsWith('ICE') ? 'ICE' : 'HL'}</i>{identityLabel}</div>
<button type="button" className="back" onClick={onBack}></button>
</nav>
<div className="grid">
<div className="hero glass">
<div className="topline"><span className="lbl"> · </span><span className="tag"> · </span></div>
<div className="day"> {dayNumber} </div>
<div className="sub"> 2025-04-26 · <b>{duty}</b> · </div>
<div className="topline"><h1></h1><span className="health"><i/></span></div>
<div className="day"><span></span> {dayNumber} </div>
<div className="sub"><b>{duty}</b></div>
<div className="clock" ref={clockRef}>--:--:--.---</div>
<div className="broadcast"><div className="hd"><span className="lbl"> 广 · </span><span className="live"><i/></span></div><div className="feed">{broadcasts.map(item => <div className="b-item" key={item.id} style={{ '--bc': BROADCAST_COLOR[item.domain] || '#a8bcff' } as React.CSSProperties}><span className="d">{item.domain}</span><span className="m">{item.message}</span><span className="tm">{item.time}</span></div>)}</div></div>
<div className="stats"><div className="stat"><span className="lbl"> · </span><div className="big">{weatherLabel}<em></em></div></div><div className="stat"><span className="lbl"> · </span><div className="big">{activityCount}<em></em></div><div className="bars" style={{ height: 34, marginTop: 10 }}>{activityBars.map((value, index) => <i key={index} className={index === activityBars.length - 1 ? 'hot' : ''} style={{ height: `${Math.max(4, Math.round(value / maxBar * 100))}%` }}/>)}</div></div></div>
<div className="broadcast"><div className="hd"><h2>广</h2><span className="verified"></span></div><div className="feed">{broadcasts.map(item => <div className="b-item" key={item.id} style={{ '--bc': BROADCAST_COLOR[item.domain] || '#a8bcff' } as React.CSSProperties}><span className="d">{item.domain}</span><span className="m">{item.message}</span><span className="tm">{item.time}</span></div>)}</div></div>
<div className="stats"><div className="stat"><h2></h2><div className="big">{weatherLabel}<em></em></div></div><div className="stat"><h2></h2><div className="big">{activityCount}<em></em></div><div className="bars" style={{ height: 34, marginTop: 10 }}>{activityBars.map((value, index) => <i key={index} className={index === activityBars.length - 1 ? 'hot' : ''} style={{ height: `${Math.max(4, Math.round(value / maxBar * 100))}%` }}/>)}</div></div></div>
</div>
<div className="operations glass">
<section className="registry"><div className="hd"><h2></h2><span>{channels.length} </span></div><div className="rows">{channels.map(channel => <div className="row" key={channel.title}><span className="dot" style={{ color: channel.color, background: channel.color }}/><div className="t"><b>{channel.title}</b><span>{channel.meta}</span></div><span className="state" style={{ '--tc': channel.tone } as React.CSSProperties}>{channel.tag}</span><span className="num">{channel.count}</span></div>)}</div></section>
<section className="system-list"><div className="hd"><h2></h2><span></span></div><div className="rows">{systems.map(system => <div className="row" key={system.title}><div className="t"><b>{system.title}</b><span>{system.meta}</span></div><span className="state" style={system.tone ? { '--tc': system.tone } as React.CSSProperties : undefined}>{system.tag}</span></div>)}</div></section>
</div>
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}> {channels.length} </span></div><div className="rows">{channels.map(channel => <div className="row" key={channel.title}><span className="dot" style={{ color: channel.color, background: channel.color }}/><div className="t"><b>{channel.title}</b><span>{channel.meta}</span></div><span className="tag" style={{ '--tc': channel.tone } as React.CSSProperties}>{channel.tag}</span><span className="num">{channel.count}</span></div>)}</div></div>
<div className="card glass"><div className="hd"><span className="lbl"> </span><span className="tag" style={{ '--tc': 'var(--good)' } as React.CSSProperties}></span></div><div className="rows">{systems.map(system => <div className="row" key={system.title}><div className="t"><b>{system.title}</b><span>{system.meta}</span></div><span className="tag" style={system.tone ? { '--tc': system.tone } as React.CSSProperties : undefined}>{system.tag}</span></div>)}</div></div>
</div>
<div className="finisher glass"><small></small>{FINISHES.map(item => <button key={item.id} type="button" className={finish === item.id ? 'on' : ''} onClick={() => pickFinish(item.id)}>{item.name}</button>)}</div>
<footer className="workbench-footer"><button type="button" className="footer-back" onClick={onBack}> </button><div className="finisher glass"><small></small>{FINISHES.map(item => <button key={item.id} type="button" className={finish === item.id ? 'on' : ''} onClick={() => pickFinish(item.id)}>{item.name}</button>)}</div></footer>
</div>
</div>
}

View file

@ -368,3 +368,125 @@
.trad-surface .broadcast .live i { animation:none; }
.trad-surface .b-item { animation:none; }
}
/* 2026-08-21 · 传统工作台正式界面:开放式玻璃工作区,不使用仪表盘卡片堆叠。 */
.trad-surface {
--glass-hi:rgba(16,28,48,.54);
--glass-lo:rgba(7,15,29,.28);
--glass-edge:rgba(190,215,255,.2);
--glass-inner:rgba(255,255,255,.16);
--rule:color-mix(in srgb,var(--ink) 15%,transparent);
}
.trad-surface[data-finish="porcelain"],
.trad-surface[data-finish="snow"] {
--glass-hi:rgba(255,255,255,.45);
--glass-lo:rgba(240,247,255,.18);
--glass-edge:rgba(255,255,255,.72);
--glass-inner:rgba(255,255,255,.86);
--rule:rgba(27,35,64,.14);
}
.trad-surface .sky::after { opacity:.18; }
.trad-surface .glass {
border-radius:11px;
background:linear-gradient(155deg,var(--glass-hi),var(--glass-lo));
-webkit-backdrop-filter:blur(20px) saturate(1.18);
backdrop-filter:blur(20px) saturate(1.18);
box-shadow:inset 0 1px 0 var(--glass-inner),0 12px 36px rgba(1,5,14,.2);
}
.trad-surface .glass::before { background:linear-gradient(120deg,rgba(255,255,255,.07),transparent 28%); }
.trad-surface .glass::after { display:none; }
.trad-surface .stage {
width:100%; max-width:1580px;
padding:max(54px,var(--pad,18px)) var(--pad,18px) var(--pad,18px);
gap:0;
grid-template-rows:62px minmax(0,1fr) 60px;
}
.trad-surface .nav {
z-index:4; gap:24px; padding:0 18px; border-radius:11px 11px 0 0;
border-bottom-color:var(--rule); box-shadow:inset 0 1px 0 var(--glass-inner);
}
.trad-surface .nav .brand { min-width:180px; font-size:16px; letter-spacing:.04em; }
.trad-surface .nav .brand small { display:block; margin:2px 0 0; font-size:8px; letter-spacing:.17em; }
.trad-surface .nav .tabs { align-self:stretch; gap:22px; margin-left:10px; overflow:visible; }
.trad-surface .nav .tabs button {
position:relative; padding:0 2px; border-radius:0; font-size:12px; font-weight:570; letter-spacing:.02em;
}
.trad-surface .nav .tabs button.on { background:transparent; }
.trad-surface .nav .tabs button.on::after {
content:''; position:absolute; left:1px; right:1px; bottom:0; height:2px; background:var(--accent);
box-shadow:0 0 12px color-mix(in srgb,var(--accent) 54%,transparent);
}
.trad-surface .nav .me { font-size:11px; font-weight:540; }
.trad-surface .nav .me i { width:26px; height:26px; background:color-mix(in srgb,var(--accent) 24%,transparent); color:var(--ink); box-shadow:none; }
.trad-surface .grid {
gap:0; grid-template-columns:minmax(0,1.5fr) minmax(370px,.96fr); grid-template-rows:minmax(0,1fr);
border:1px solid var(--glass-edge); border-top:0; border-bottom:0;
background:color-mix(in srgb,var(--bg-base) 46%,transparent);
-webkit-backdrop-filter:blur(14px) saturate(1.08); backdrop-filter:blur(14px) saturate(1.08);
}
.trad-surface .hero {
grid-row:auto; min-width:0; padding:38px 34px 24px; border:0; border-right:1px solid var(--rule); border-radius:0;
background:transparent; box-shadow:none; backdrop-filter:none; -webkit-backdrop-filter:none;
}
.trad-surface .hero::before,.trad-surface .hero::after { display:none; }
.trad-surface .hero .topline { align-items:center; }
.trad-surface .hero .topline h1,.trad-surface .broadcast h2,.trad-surface .stat h2,.trad-surface .operations h2 {
margin:0; color:var(--ink); font-size:14px; font-weight:650; letter-spacing:.01em;
}
.trad-surface .hero .health { display:inline-flex; align-items:center; gap:8px; color:var(--good); font-size:11px; }
.trad-surface .hero .health i { width:7px; height:7px; border-radius:50%; background:var(--good); box-shadow:0 0 9px var(--good); }
.trad-surface .hero .day { margin-top:38px; font-size:clamp(32px,4.2vw,54px); font-weight:630; letter-spacing:.02em; text-shadow:none; }
.trad-surface .hero .day span { margin-right:12px; font-size:.58em; font-weight:530; letter-spacing:.12em; }
.trad-surface .hero .sub { margin-top:15px; font-size:13px; font-weight:520; letter-spacing:.02em; }
.trad-surface .hero .clock { margin-top:23px; font-size:13px; font-weight:520; letter-spacing:.12em; }
.trad-surface .broadcast { flex:none; margin-top:42px; }
.trad-surface .broadcast .hd { padding-bottom:13px; border-bottom:1px solid var(--rule); }
.trad-surface .broadcast .verified { color:var(--good); font-size:10px; }
.trad-surface .broadcast .feed { margin-top:8px; gap:0; }
.trad-surface .b-item { padding:12px 4px; border:0; border-bottom:1px solid var(--rule); border-radius:0; background:transparent; }
.trad-surface[data-finish="porcelain"] .b-item,.trad-surface[data-finish="snow"] .b-item { background:transparent; border-color:var(--rule); }
.trad-surface .b-item .d { font-size:10.5px; }
.trad-surface .b-item .m { font-size:12.5px; }
.trad-surface .hero .stats { margin-top:auto; gap:0; border-top:1px solid var(--rule); }
.trad-surface .hero .stat { min-width:0; padding:22px 6px 4px; border:0; border-radius:0; background:transparent; }
.trad-surface .hero .stat + .stat { padding-left:28px; border-left:1px solid var(--rule); }
.trad-surface[data-finish="porcelain"] .hero .stat,.trad-surface[data-finish="snow"] .hero .stat { background:transparent; border-color:var(--rule); }
.trad-surface .hero .stat .big { margin-top:15px; font-size:25px; font-weight:600; }
.trad-surface .hero .stat .big em { font-weight:480; }
.trad-surface .operations {
min-width:0; padding:0 28px; border:0; border-radius:0; background:transparent; box-shadow:none;
backdrop-filter:none; -webkit-backdrop-filter:none; overflow:auto;
}
.trad-surface .operations::before,.trad-surface .operations::after { display:none; }
.trad-surface .operations section { padding:36px 0 26px; }
.trad-surface .operations section + section { border-top:1px solid var(--rule); }
.trad-surface .operations .hd { display:flex; justify-content:space-between; gap:16px; padding:0 0 15px; border-bottom:1px solid var(--rule); }
.trad-surface .operations .hd > span { color:var(--ink-faint); font-size:10.5px; }
.trad-surface .row { padding:14px 0; border-radius:0; border-bottom:1px solid var(--rule); }
.trad-surface .row:hover { background:color-mix(in srgb,var(--accent) 6%,transparent); }
.trad-surface .row .t b { font-size:12.5px; font-weight:610; }
.trad-surface .row .t span { font-size:10.5px; font-weight:480; }
.trad-surface .row .state { min-width:54px; color:var(--tc,var(--good)); font-size:10.5px; text-align:right; }
.trad-surface .workbench-footer {
display:flex; align-items:center; justify-content:space-between; gap:18px; padding:0 18px;
border:1px solid var(--glass-edge); border-top-color:var(--rule); border-radius:0 0 11px 11px;
background:linear-gradient(155deg,var(--glass-hi),var(--glass-lo));
-webkit-backdrop-filter:blur(20px) saturate(1.18); backdrop-filter:blur(20px) saturate(1.18);
}
.trad-surface .footer-back { border:0; background:transparent; color:var(--ink-sub); font-size:11.5px; cursor:pointer; }
.trad-surface .finisher { justify-self:auto; padding:0; border:0; background:transparent; box-shadow:none; backdrop-filter:none; }
.trad-surface .finisher::before,.trad-surface .finisher::after { display:none; }
.trad-surface .finisher small { letter-spacing:.05em; }
.trad-surface .finisher button { padding:8px 10px; border-radius:7px; font-size:10.5px; letter-spacing:.02em; font-weight:560; }
.trad-surface .finisher button.on { background:color-mix(in srgb,var(--accent) 14%,transparent); box-shadow:inset 0 -2px var(--accent); }
@media (max-width: 980px) {
.trad-surface .stage { grid-template-rows:auto minmax(0,1fr) auto; }
.trad-surface .nav { flex-wrap:wrap; padding-block:12px; }
.trad-surface .nav .brand { min-width:120px; }
.trad-surface .nav .tabs { order:3; width:100%; height:34px; overflow-x:auto; }
.trad-surface .grid { grid-template-columns:1fr; overflow:auto; }
.trad-surface .hero { border-right:0; border-bottom:1px solid var(--rule); min-height:620px; }
.trad-surface .operations { min-height:520px; }
.trad-surface .workbench-footer { flex-wrap:wrap; padding-block:9px; }
}

View file

@ -11,6 +11,9 @@ body {
font-weight: 430;
-webkit-font-smoothing: antialiased;
}
.visual-qa-shell { min-height:100%; padding:70px 5vw; color:var(--content-secondary); background:linear-gradient(145deg,#07111f,#102744); }
.visual-qa-shell > header { display:flex; justify-content:space-between; max-width:1480px; margin:0 auto 22px; color:#eef4ff; }
.visual-qa-shell > main { max-width:1480px; margin:0 auto; }
button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
::-webkit-scrollbar { width: 11px; height: 11px; }
@ -469,6 +472,40 @@ svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-lineca
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; }
}
/* 2026-08-21 · 正式公众授权界面:列表与证据层级替代大卡片仪表盘。 */
.human-authorization-layout {
grid-template-columns:minmax(0,1.1fr) minmax(330px,.7fr);
gap:0;
border:1px solid var(--panel-edge);
border-radius:12px;
overflow:hidden;
background:color-mix(in srgb,var(--panel-bg) 62%,transparent);
box-shadow:0 18px 46px color-mix(in srgb,var(--primitive-shadow) 24%,transparent);
}
.human-authorization-layout > .plain-panel {
border:0; border-radius:0; background:transparent; box-shadow:none;
}
.authorization-primary { grid-row:auto; border-right:1px solid var(--panel-edge) !important; }
.collaboration-live { border-bottom:1px solid var(--panel-edge) !important; }
.external-ai-gateway-panel { grid-column:1 / -1; border-top:1px solid var(--panel-edge) !important; }
.human-authorization-layout .plain-panel > header { padding-bottom:16px; border-bottom:1px solid var(--panel-edge); }
.authorization-empty { min-height:190px; margin-top:0; border:0; border-radius:0; }
.collaboration-summary,.gateway-summary { gap:0; border-bottom:1px solid var(--panel-edge); }
.collaboration-summary div,.gateway-summary div { border-radius:0; background:transparent; }
.collaboration-summary div + div,.gateway-summary div + div { border-left:1px solid var(--panel-edge); }
.status-chip { border-radius:7px; padding:5px 8px; }
.technical-details { margin-top:5px; }
.technical-details summary { width:max-content; color:var(--content-muted); font-size:10px; cursor:pointer; }
.technical-details code { display:block; max-width:280px; overflow:hidden; margin-top:4px; color:var(--content-muted); font-size:9px; text-overflow:ellipsis; white-space:nowrap; }
.gateway-catalog-grid > section,.gateway-exposure-control,.gateway-connection-details {
border-radius:8px; background:color-mix(in srgb,var(--primitive-glass) 38%,transparent);
}
@media (max-width:880px) {
.human-authorization-layout { grid-template-columns:1fr; }
.authorization-primary { border-right:0 !important; border-bottom:1px solid var(--panel-edge) !important; }
}
/* ===== 布局收展(学 Tolaria侧栏可收正文与知识库拿回宽度 ===== */
.app-shell.sidebar-collapsed { grid-template-columns: 80px minmax(0, 1fr); }
.sidebar-toggle { width: 30px; height: 30px; margin-left: auto; border-radius: 8px; }