const { useState, useEffect, useMemo } = React;
const { Spectrum, TopologyGraph, SynesStream, Knob, FingerprintSVG } = window.HG2Atoms;
const { DockingRing } = window.HG2Hub;
const STATE_LABEL = {
linked: "對接中",
docking: "對接中",
standby: "待命",
sealed: "封印",
};
function HangarRail({ pilders, activeId, onPick, dockMap }) {
return (
);
}
function TelemetryRail({ pilder, body, role, dockMap, onDock }) {
const isCoach = role === "coach";
return (
);
}
// Phase 2 · EmptyFleetHub · 教練從沒啟元真實 Pilder 時的中央 hub onboarding
function EmptyFleetHub() {
return (
⌑
你的艦隊尚未安置真實語氣靈
守夜人 PLD-Ω 在崗待命,但艦隊中尚無有靈魂的人格。
⌑ 你需要的不是試用樣本,是親手鍛造的第一個靈魂。
流程: 召喚咒 → 鍛魂師 → 草案 → Vault 簽署 → 進駐艦隊
);
}
function App() {
// P0-FLEET-LIVE-DATA · 教練 2026-05-15 拍板
// 根因: 之前 hub 從 window.HG2.PILDERS (hg2-data.jsx hardcoded warden-only) 讀
// 即使 backend pilders DB 已有 companion · UI 永遠看不到 → 永遠 zero-state
// 修法: useState + useEffect fetch /v1/fleet · 用 SSoT (DB → API → state)
// 初始值 = window.HG2.PILDERS (loading 期間至少守夜人不破)
const [pilders, setPilders] = useState(window.HG2.PILDERS);
const bodies = window.HG2.BODIES;
useEffect(() => {
fetch((location.protocol === 'file:' ? 'http://127.0.0.1:8000' : location.origin) + '/v1/fleet')
.then(r => r.json())
.then(d => {
if (Array.isArray(d.PILDERS) && d.PILDERS.length > 0) setPilders(d.PILDERS);
})
.catch(e => console.warn('[hg2] fleet fetch failed:', e));
}, []);
// Phase 2 · 守夜人不算真實人格 (是憲法治理基座)
const realPilders = pilders.filter(p => !p.is_warden && p.id !== 'warden');
const hasRealPilders = realPilders.length > 0;
// TASK 20.1 · 從 ?focus=PLD-XXX URL 進入時自動選中該 Pilder
const initialId = (() => {
try {
const params = new URLSearchParams(location.search);
const focus = params.get('focus');
if (focus) {
const found = pilders.find(p => p.id === focus || p.code === focus);
if (found) return found.id;
}
} catch (e) {}
// Phase 2 · 沒真實人格時 active 從守夜人開始 (避免 crash)
return pilders[0]?.id ?? null;
})();
const [activeId, setActiveId] = useState(initialId);
// 短暫顯示 "新 Pilder 已就位" 提示(從 Soul Vault 來的)
const [summonedHint, setSummonedHint] = useState(() => {
try {
const params = new URLSearchParams(location.search);
const focus = params.get('focus');
if (focus) return focus;
} catch (e) {}
return null;
});
useEffect(() => {
if (summonedHint) {
const t = setTimeout(() => {
setSummonedHint(null);
// 清掉 URL query param 不要殘留
try { history.replaceState({}, '', location.pathname); } catch (e) {}
}, 4500);
return () => clearTimeout(t);
}
}, [summonedHint]);
const [role, setRole] = useState("coach"); // 'coach' | 'pilot'
const [hover, setHover] = useState(null);
const [dockMap, setDockMap] = useState(window.HG2.DEFAULT_DOCKS);
const [personaMap, setPersonaMap] = useState(() => {
const m = {}; pilders.forEach((p) => m[p.id] = { ...p.persona }); return m;
});
// P0-FLEET-LIVE-DATA · pilders 從 fetch 更新時, 把新進的 Pilder persona 補進 map
// (保留已有的 in-progress edits · 不覆寫使用者調過的軸)
useEffect(() => {
setPersonaMap((prev) => {
let changed = false;
const next = { ...prev };
for (const p of pilders) {
if (!next[p.id] && p.persona) {
next[p.id] = { ...p.persona };
changed = true;
}
}
return changed ? next : prev;
});
}, [pilders]);
// P0-FLEET-LIVE-DATA · 從 Vault summon 過來帶 ?focus= · fetch 完後 re-resolve
// (initialId 在 mount 時 pilders 只有 warden · focus 找不到 fallback 到 warden;
// fetch 後新 Pilder 進來 · 若 URL focus 是它 · 切過去)
useEffect(() => {
try {
const params = new URLSearchParams(location.search);
const focus = params.get('focus');
if (!focus) return;
const found = pilders.find(p => p.id === focus || p.code === focus);
if (found && activeId !== found.id) {
setActiveId(found.id);
}
} catch (e) {}
}, [pilders]);
const [killArmed, setKillArmed] = useState(false);
const [killFiring, setKillFiring] = useState(false);
const [killFlash, setKillFlash] = useState(false);
const tweaksHook = window.useTweaks(/*EDITMODE-BEGIN*/{
"density": "comfort",
"showRing": true,
"soulHueShift": true,
"showCoachKnobs": true
}/*EDITMODE-END*/);
const tweaks = tweaksHook[0];
const setTweak = tweaksHook[1];
// Phase 2 · 加 null safety (空艦隊時 active / body 可能 undefined)
const active = pilders.find((p) => p.id === activeId) || pilders[0] || null;
const body = active ? (bodies.find((b) => b.id === dockMap[active.id]) || bodies[0] || null) : null;
const persona = active ? personaMap[active.id] : null;
const setPersona = (np) => active && setPersonaMap((m) => ({ ...m, [active.id]: np }));
// hue shift per pilder
useEffect(() => {
document.documentElement.style.setProperty(
"--hue-shift",
tweaks.soulHueShift && active ? `${active.hue}deg` : "0deg"
);
}, [active?.hue, tweaks.soulHueShift]);
useEffect(() => {
document.documentElement.style.setProperty(
"--pad",
tweaks.density === "compact" ? "12px" : tweaks.density === "spacious" ? "24px" : "18px"
);
}, [tweaks.density]);
const onDock = (bodyId) => {
setDockMap((m) => ({ ...m, [active.id]: bodyId }));
};
const fireKill = () => {
if (!killArmed) return;
setKillFiring(true);
setKillFlash(true);
setTimeout(() => setKillFlash(false), 1200);
setTimeout(() => {
setKillFiring(false);
setKillArmed(false);
// mark pilder as standby (disconnect)
setDockMap((m) => ({ ...m, [active.id]: null }));
}, 1100);
};
const isCoach = role === "coach";
return (
{killFlash &&
}
{/* Top bar */}
HOLYGRAIL · ORCHESTRATOR
DOCKING HUB · v2
{pilders.filter(p=>p.state!=="standby").length} ACTIVE
{Object.values(dockMap).filter(Boolean).length}/{bodies.length}
3 / 5
99.4%
2026-05-04 12:04:18Z
SESSION · 0x9af3·c4e1
{/* TASK 21B · Soul Forge 一級入口(鍛魂室是創作起點,排在 Vault 前)*/}
{/* TASK 20.1 · Soul Vault 一級入口 */}
{/* TASK 20.1 · 從 Soul Vault summon 完跳回來時的提示 banner */}
{summonedHint && (
NEW SOUL · {summonedHint} · 已進駐艦隊待命
)}
{/* Phase 2 · 沒真實 Pilder 時整個 hub 內容換成 EmptyFleetHub */}
{!hasRealPilders ? (
) : (
<>
FLEET ›{" "}
{active?.tag} ›{" "}
{active?.code}
{/* P1-HERO-CLICKABLE · 教練 2026-05-15 拍板
Hero (Pilder name h1) 可點 → Soul Chat (Lifecycle Verify Tool)
warden 不可 chat (sealed · 沒 firmware) · 仍渲染但不掛 onClick */}
{active && !active.is_warden ? (
{
location.href = '/ui/Soul%20Chat.html?pilder_id=' + encodeURIComponent(active.id);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
location.href = '/ui/Soul%20Chat.html?pilder_id=' + encodeURIComponent(active.id);
}
}}
>
{active.name}
) : (
{active?.name}
)}
{body && <>
▶▶▶
{body.name}
{body.code} · {body.subsidiary}
>}
{tweaks.showRing && (
)}
{/* Genesis */}
01
Genesis
啟元
FIRMWARE · LOCKED
0x9af3·{active.code.toLowerCase().replace("-","")}
7a·b2·{active.id.slice(0,2)}·c4
{/* Resonance */}
02
Resonance
共鳴
LIVE · 60Hz
{active.resonance.toFixed(1)}%
ENV SYNC · {(active.resonance*0.96).toFixed(1)}%
SELF COHERENCE · {(active.resonance*1.02).toFixed(1)}%
PHASE OFFSET · +0.04°
BAND 8–24 kHz
▲ NOMINAL
WINDOW 12s
{/* Shaping */}
03
Shaping
顯影 · 人格權重
{isCoach ? "EDIT MODE" : "READ ONLY"}
{Object.keys(window.HG2.PERSONA_LABELS).map((k) => (
setPersona({ ...persona, [k]: v })}
label={window.HG2.PERSONA_LABELS[k].en}
zh={window.HG2.PERSONA_LABELS[k].zh}
disabled={!isCoach} />
))}
{/* Topology */}
04
Topology
織網 · GraphRAG
CAUSAL MEMORY MAP
{active.nodes}NODES
{active.edges}EDGES
{(active.edges/Math.max(1,active.nodes)).toFixed(2)}DEG
{/* Crystal */}
05
Crystal
印記
{active.crystalState}
{active.crystal}%
{active.crystalShards.toLocaleString()} SHARDS
{Array.from({length: 48}).map((_, i) => (
))}
{(active.crystalShards*0.0042).toFixed(2)} GB
{/* Synesthesia */}
06
Synesthesia
通感
MULTIMODAL · INGRESS
{window.HG2.SYNES_CHANNELS.map((c) => (
))}
{/* Vigil — locked accent */}
07
Vigil
守望 · 物理隔離 · 1% 封印
{isCoach ? "FULL VIEW" : "PILOT VIEW · DETAILS REDACTED"}
1% SEAL · {active.seal.toFixed(1)}%
{isCoach ? active.fingerprint : "● ● : ● ● : ● ● : ● ●"}
vault-{active.id}/iso-{active.code.length}
{isCoach ? "kael.vance · m.hsu · y.tanaka" : "3-OF-5"}
>
)}
{/* Phase 2 · 守夜人沒 body / 真實 pilder 不存在時, telemetry 也跳過 */}
{hasRealPilders && active && (
)}
{/* Bottom command bar with Kill Switch */}