17 Commits

Author SHA1 Message Date
9276c7cd75 Markets: the price chart is AP3X/USD across every pool, not one pair
The chart card read one DexScreener pair (bAP3X/USDC on Aerodrome). It now
draws AP3X in USD aggregated over every pool with at least $1K of liquidity
- four on Base (Aerodrome USDC and WETH, Uniswap and QuickSwap cbBTC) and
Minswap's cAP3X/ADA on Cardano - from GeckoTerminal's per-pool USD candles:
price liquidity-weighted (a pool with no trade holds its last close), volume
summed. 15m / 1h / 4h / 1D, an OHLC + volume readout, and a footer naming the
pools. GeckoTerminal is keyless and rate-limited, so the pool list (6 h) and
candles (3 min) are kept in localStorage; when it is busy the card shows the
largest pool's DexScreener chart and retries. VyFi and LBank are not in its
feed and say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:28:38 -05:00
de5b01cd01 Apex Universe: daily figures 2026-10-02 2026-10-02 06:15:06 +00:00
2373ea5299 Apex Universe: daily figures 2026-10-01 2026-10-01 06:15:04 +00:00
a3960a5bf9 Apex Universe: daily figures 2026-09-30 2026-09-30 06:15:03 +00:00
7347a647e8 Apex Universe: daily figures 2026-09-29 2026-09-29 06:15:05 +00:00
78f1df4934 Apex Universe: daily figures 2026-09-28 2026-09-28 06:15:03 +00:00
16126e9203 Merge pull request 'Fusion AI launch film: 'Now live' as in the site is live' (#4) from fix/launch-film-live into main 2026-09-27 06:52:15 +00:00
830d021f94 Fusion AI launch film: 'Now live' as in the site is live
The closing line was voiced as live (to live). Re-voiced so it rhymes
with five (checked against Brian's own 'five' and 'drive' by formant),
everything else unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:52:00 -05:00
d7b0434e58 Merge pull request 'Fusion AI launch film: HAL 8 card and the widget lightbox' (#3) from feat/fusion-launch-film into main 2026-09-27 06:34:17 +00:00
dfa72eec45 Fusion AI launch film: the HAL 8 card and the widget's lightbox
A 32 s trailer rendered in Blender from the Apex Universe scene. The
pane in it is the real widget replaying answers Fusion AI gave on the
live route, voiced by ElevenLabs Brian, with an original score.
fusion/launch/ holds the 16:9 cut, its poster and English captions.
The HAL 8 section gets a 'Meet Fusion AI' card (Talk to Fusion AI opens
the widget through a fusion:open event), and the widget's welcome
screen a 'Watch the launch film' chip that plays it in a lightbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 01:34:11 -05:00
c97d524025 Apex Universe: daily figures 2026-09-27 2026-09-27 06:15:05 +00:00
b9b9fe06d8 Merge pull request 'Fusion AI, powered by HAL 8: red eye, Standard | Incognito, code, media and voice' (#2) from feat/fusion-ai-hal8 into main 2026-09-27 00:56:47 +00:00
23b05a72da Fusion AI: play HAL 8 video links inline
HAL 8's signed file links carry no extension, so a clip linked as
[video](...) rendered as a plain link. A HAL 8 file labelled video/clip
is now a player, and a HAL 8 file embedded as an image that will not
decode is swapped for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:56:23 -05:00
b2105ffa46 Fusion AI: honest Incognito copy and the README section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:18:53 -05:00
716615a1d2 Fusion AI, powered by HAL 8
The hub's assistant becomes a consumer HAL 8 chat:
- The button is HAL 8's red eye (the real logo); the header reads POWERED BY HAL 8.
- One control, a Standard | Incognito pill. Standard keeps the conversation in this browser;
  Incognito keeps nothing and asks the server for no retention.
- Always on, no toggles: chat and code (copy; Python runs in the browser via Pyodide), images and
  video inline, web answers with their sources, voice in (speech recognition) and read-aloud.
- Streams from HAL 8's public Fusion endpoint (hal8.odyssey-works.io/v1/public/fusion/chat); the
  browser never holds a key. Limits come back as a friendly "see you tomorrow" card.
- A small safe markdown renderer: everything escaped, https-only links and media, HAL 8-relative
  file links resolved against HAL 8.
- tools/fusion_inject.py replaces the old assistant block idempotently (between /*F8-BEGIN*/ and
  /*F8-END*/) and inlines fusion/fusion.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 18:50:44 -05:00
f50457b1c1 Apex Universe: daily figures 2026-09-26 2026-09-26 23:03:04 +00:00
103c7903ff Merge pull request 'Apex Universe: the ecosystem as a live star system, below Overview' (#1) from feat/apex-universe into main 2026-09-26 23:02:50 +00:00
9 changed files with 574 additions and 10 deletions

View File

@@ -69,16 +69,38 @@ Two things are fetched at runtime rather than bundled:
- **three.js** `0.160.0` from `cdn.jsdelivr.net` — powers the Fusion
Chamber 3D visual (§12). If the CDN is unreachable that section
degrades; the rest of the page is unaffected.
- **The AI assistant widget** posts to `window.FUSION_AI_ENDPOINT`,
falling back to `text.pollinations.ai`. See the note below.
- **Fusion AI**, the assistant widget: `fusion/fusion.js`, loaded on
demand. It posts to HAL 8's public route (see the note below), and its
Python **Run** button loads Pyodide from `cdn.jsdelivr.net` on first use.
## Note on the AI assistant
## Fusion AI (powered by HAL 8)
The chat widget in the bottom-right sends visitor messages to a
third-party inference endpoint by default. `window.FUSION_AI_ENDPOINT`
can be repointed at HAL 8's own OpenAI-compatible API
(`https://hal8.odyssey-works.io/v1`) so conversations stay on
Odyssey Works infrastructure.
The red-eye button in the bottom-right is Fusion AI. The page holds no key.
It posts to `https://hal8.odyssey-works.io/v1/public/fusion/chat`, and
`window.FUSION_AI_ENDPOINT` overrides that for testing. The route runs
HAL 8's headless pipeline under its own capped account: $1 a day for
everyone, and per visitor 40 messages, 6 images and 1 video a day. Its full
contract is `docs/FUSION.md` in the HAL 8 repository.
- **Standard | Incognito** is the only control.
- Standard is `hal8-auto`; the conversation is kept in this browser only.
- Incognito is a Venice private model; nothing is kept, and generated
media is deleted after a day.
- Chat, code, web search, images, video and voice are always on.
- Python code blocks run in the visitor's browser.
The launch film lives in `fusion/launch/`. It includes:
- `fusion-ai-launch.mp4`, the 32-second, 16:9 cut shown on the HAL 8 page (the "Meet Fusion AI" card) and in the widget's "Watch the launch film" lightbox;
- `poster.jpg`, its poster frame;
- `fusion-ai-launch.vtt`, its subtitles.
The film was rendered in Blender from the Apex Universe scene. The Fusion pane in it is the real widget, playing answers Fusion AI actually gave. The voice is ElevenLabs "Brian", and the score is original.
The widget is `fusion/fusion.js` and `fusion/fusion.css`. After editing
either, run `python3 tools/fusion_inject.py`: it inlines the CSS and
re-stamps the module version inside `index.html`, and re-running it
replaces rather than duplicates.
## Markets section — live feeds

93
fusion/fusion.css Normal file
View File

@@ -0,0 +1,93 @@
.ai-fab,.ai-pane{display:none!important}
.f8-root button{font:inherit;cursor:pointer}
.f8-fab{position:fixed;right:28px;bottom:28px;width:58px;height:58px;border-radius:15px;padding:0;border:0;background:#000;z-index:101;overflow:hidden;
box-shadow:0 0 0 1px rgba(255,255,255,.08),0 10px 30px rgba(0,0,0,.5),0 0 34px -6px rgba(255,42,26,.75);animation:f8breath 3.6s ease-in-out infinite;transition:transform .2s}
.f8-fab:hover{transform:scale(1.05)}
.f8-fab img{width:100%;height:100%;display:block}
.f8-fab-x{display:none;position:absolute;inset:0;align-items:center;justify-content:center;font-size:28px;color:var(--ink);background:var(--paper-2)}
.f8-open .f8-fab{animation:none;box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 0 1px var(--rule)}
.f8-open .f8-fab-x{display:flex}
@keyframes f8breath{50%{box-shadow:0 0 0 1px rgba(255,255,255,.08),0 10px 30px rgba(0,0,0,.5),0 0 48px -2px rgba(255,42,26,.95)}}
.f8-pane{position:fixed;right:28px;bottom:100px;width:420px;max-width:calc(100vw - 40px);height:640px;max-height:calc(100vh - 130px);z-index:100;
background:var(--paper-2);border:1px solid var(--rule);border-radius:14px;box-shadow:0 24px 70px rgba(0,0,0,.65);display:flex;flex-direction:column;overflow:hidden}
.f8-pane[hidden]{display:none}
.f8-head{display:flex;justify-content:space-between;align-items:flex-start;padding:18px 18px 10px}
.f8-title{font-size:24px;line-height:1;letter-spacing:-.01em}
.f8-power{display:flex;align-items:center;gap:7px;margin-top:8px;font-family:var(--f-mono);font-size:9.5px;letter-spacing:.22em;color:var(--ink-3)}
.f8-power img{width:14px;height:14px;border-radius:3px}
.f8-head-actions{display:flex;gap:4px}
.f8-head-actions button{width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ink-3);background:none;border:0;border-radius:8px;font-size:22px;padding:0}
.f8-head-actions button:hover{background:rgba(255,255,255,.05);color:var(--ink)}
.f8-modebar{padding:4px 18px 12px;border-bottom:1px solid var(--rule-2)}
.f8-pill{display:inline-flex;padding:4px;gap:2px;border-radius:999px;background:rgba(255,255,255,.025);border:1px solid var(--rule)}
.f8-pill button{display:inline-flex;align-items:center;gap:7px;padding:8px 18px;border-radius:999px;border:0;background:none;color:var(--ink-3);font-family:var(--f-display);font-size:14px;transition:background .2s,color .2s}
.f8-pill button.on{background:#EAF3EC;color:#07100C;font-weight:500}
.f8-incog-note{display:none;align-items:center;gap:8px;padding:9px 18px;font-family:var(--f-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ink-2);background:rgba(255,255,255,.03);border-bottom:1px solid var(--rule-2)}
.f8-incognito .f8-incog-note{display:flex}
.f8-incognito .f8-pane{background:#0A1411}
.f8-body{flex:1;overflow:auto;padding:18px;display:flex;flex-direction:column;gap:14px}
.f8-empty p{font-size:19px;line-height:1.4;margin:6px 0 18px;color:var(--ink)}
.f8-sugg{display:grid;gap:8px}
.f8-sugg button{text-align:left;padding:11px 13px;border:1px solid var(--rule);border-radius:10px;background:rgba(255,255,255,.02);color:var(--ink-2);font-size:13.5px;transition:border-color .2s}
.f8-sugg button:hover{border-color:var(--emerald)}
.f8-sugg b{display:block;font-family:var(--f-mono);font-weight:400;font-size:9.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--emerald);margin-bottom:3px}
.f8-u{align-self:flex-end;max-width:82%;padding:10px 13px;border-radius:12px 12px 4px 12px;background:rgba(255,255,255,.06);font-size:14px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
.f8-a{font-size:14px;line-height:1.55;color:var(--ink);word-break:break-word}
.f8-a p{margin:0 0 8px} .f8-a ul,.f8-a ol{margin:0 0 8px;padding-left:20px;color:var(--ink-2)} .f8-a h4{margin:10px 0 6px;font-family:var(--f-display);font-size:15px}
.f8-a a{color:var(--emerald)} .f8-a code{font-family:var(--f-mono);font-size:12.5px;background:rgba(255,255,255,.06);padding:1px 5px;border-radius:4px}
.f8-meta{display:flex;gap:4px;margin-top:4px}
.f8-meta button,.f8-media-bar a{background:none;border:0;color:var(--ink-4);padding:4px;display:inline-flex}
.f8-meta button:hover,.f8-media-bar a:hover{color:var(--ink)}
.f8-code{border:1px solid var(--rule);border-radius:10px;overflow:hidden;background:#07100C;margin:6px 0}
.f8-code-h{display:flex;justify-content:space-between;align-items:center;padding:7px 10px;font-family:var(--f-mono);font-size:10px;letter-spacing:.14em;color:var(--ink-3);border-bottom:1px solid var(--rule-2);text-transform:uppercase}
.f8-code-act{display:flex;gap:6px}
.f8-code-act button{display:inline-flex;gap:5px;align-items:center;font-family:var(--f-mono);font-size:10px;letter-spacing:.1em;color:var(--ink-2);background:rgba(255,255,255,.04);border:1px solid var(--rule);border-radius:6px;padding:3px 8px}
.f8-code pre{margin:0;padding:12px;font-family:var(--f-mono);font-size:12px;line-height:1.55;color:#CFEEE2;white-space:pre-wrap;overflow-x:auto}
.f8-out{border-top:1px solid var(--rule-2);padding:8px 12px;font-family:var(--f-mono);font-size:12px;color:var(--emerald);white-space:pre-wrap}
.f8-out span{display:block;font-size:9px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-4);margin-bottom:2px}
.f8-media{position:relative;display:block;border-radius:12px;overflow:hidden;border:1px solid var(--rule);max-width:320px;margin:6px 0}
.f8-media img,.f8-media video{display:block;width:100%;max-height:360px;object-fit:cover;background:#000}
.f8-media-bar{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;font-family:var(--f-mono);font-size:10px;letter-spacing:.12em;color:var(--ink-3);background:#07100C}
.f8-limit{padding:12px 14px;border:1px solid var(--rule);border-radius:10px;background:rgba(255,255,255,.03);color:var(--ink-2)}
.f8-status{display:flex;align-items:center;gap:8px;font-family:var(--f-mono);font-size:10.5px;letter-spacing:.1em;color:var(--ink-3)}
.f8-dots{display:inline-flex;gap:4px;align-items:center;padding:4px 0}
.f8-dots i{width:6px;height:6px;border-radius:50%;background:#FF3B24;opacity:.35;animation:f8dot 1.2s infinite}
.f8-dots i:nth-child(2){animation-delay:.2s}.f8-dots i:nth-child(3){animation-delay:.4s}
@keyframes f8dot{40%{opacity:1}}
.f8-compose{display:flex;align-items:flex-end;gap:8px;padding:12px;border-top:1px solid var(--rule-2);position:relative}
.f8-compose textarea{flex:1;resize:none;min-height:44px;max-height:120px;padding:12px 14px;border-radius:12px;border:1px solid var(--rule);background:rgba(255,255,255,.03);color:var(--ink);font:inherit;font-size:14px;outline:none;line-height:1.4}
.f8-compose textarea:focus{border-color:var(--ink-4)}
.f8-mic,.f8-send{width:44px;height:44px;border-radius:12px;border:1px solid var(--rule);display:flex;align-items:center;justify-content:center;flex:none;padding:0}
.f8-mic{background:rgba(255,255,255,.03);color:var(--ink-2)}
.f8-send{background:var(--emerald);border-color:var(--emerald);color:#04110b}
.f8-listen{position:absolute;left:12px;right:112px;top:12px;bottom:12px;display:flex;align-items:center;gap:10px;padding:0 14px;border-radius:12px;background:#140b0a;border:1px solid rgba(255,42,26,.45);font-size:13px;color:var(--ink-2);overflow:hidden;white-space:nowrap}
.f8-listen[hidden]{display:none}
.f8-listen em{color:var(--ink);font-style:normal}
.f8-voice .f8-mic{background:#FF2A1A;border-color:#FF2A1A;color:#fff;animation:f8pulse 1.2s ease-in-out infinite}
@keyframes f8pulse{50%{box-shadow:0 0 0 7px rgba(255,42,26,.18)}}
.f8-wave{display:inline-flex;gap:3px;align-items:center;height:20px}
.f8-wave i{width:3px;border-radius:2px;background:#FF5A47;animation:f8wave .9s ease-in-out infinite}
.f8-wave i:nth-child(1){height:8px}.f8-wave i:nth-child(2){height:16px;animation-delay:.1s}.f8-wave i:nth-child(3){height:11px;animation-delay:.2s}.f8-wave i:nth-child(4){height:19px;animation-delay:.3s}.f8-wave i:nth-child(5){height:9px;animation-delay:.4s}.f8-wave i:nth-child(6){height:14px;animation-delay:.5s}.f8-wave i:nth-child(7){height:7px;animation-delay:.6s}
@keyframes f8wave{50%{transform:scaleY(.4)}}
.f8-toast{position:absolute;left:12px;right:12px;bottom:74px;padding:10px 12px;border-radius:10px;background:#1a2a23;border:1px solid var(--rule);font-size:12.5px;color:var(--ink);z-index:3}
@media (prefers-reduced-motion:reduce){.f8-fab,.f8-dots i,.f8-wave i,.f8-voice .f8-mic{animation:none}}
@media (max-width:700px){
.f8-pane{left:0;right:0;bottom:0;top:0;width:auto;max-width:none;height:auto;max-height:none;border-radius:0;border:0}
.f8-fab{right:18px;bottom:18px}
.f8-open .f8-fab{display:none}
}
.f8-watch{margin-top:14px;display:flex;align-items:center;gap:9px;width:100%;padding:10px 13px;border:1px solid rgba(255,42,26,.35);border-radius:10px;background:linear-gradient(90deg,rgba(255,42,26,.10),rgba(255,42,26,0));color:var(--ink);font-size:13.5px;text-align:left}
.f8-watch i{width:9px;height:9px;border-radius:50%;background:#FF2A1A;box-shadow:0 0 12px #FF2A1A;flex:none}
.f8-watch span{margin-left:auto;font-family:var(--f-mono);font-size:10.5px;color:var(--ink-3)}
.f8-watch:hover{border-color:rgba(255,42,26,.7)}
.f8-film{position:fixed;inset:0;z-index:300;background:rgba(0,0,0,.86);display:flex;align-items:center;justify-content:center;padding:24px}
.f8-film-frame{position:relative;width:min(1200px,100%);aspect-ratio:16/9;border-radius:12px;overflow:hidden;box-shadow:0 30px 90px rgba(0,0,0,.7),0 0 0 1px rgba(255,255,255,.08)}
.f8-film video{width:100%;height:100%;display:block;background:#000}
.f8-film-x{position:absolute;top:10px;right:10px;width:38px;height:38px;border-radius:50%;border:0;background:rgba(0,0,0,.55);color:#fff;font-size:24px;line-height:1;cursor:pointer}
.f8-launch{display:grid;grid-template-columns:minmax(0,1.55fr) minmax(0,1fr);gap:28px;align-items:center;margin:30px 0 8px;padding:18px;border:1px solid var(--rule);border-radius:16px;background:linear-gradient(135deg,rgba(255,42,26,.06),rgba(255,255,255,.015) 45%)}
.f8-launch-media{border-radius:12px;overflow:hidden;background:#000;aspect-ratio:16/9;box-shadow:0 18px 50px rgba(0,0,0,.5)}
.f8-launch-media video{width:100%;height:100%;display:block}
.f8-launch-kicker{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.22em;color:#FF5A47;margin-bottom:10px}
.f8-launch h3{font-family:var(--f-display);font-size:30px;line-height:1.1;margin:0 0 12px;color:var(--ink)}
.f8-launch p{color:var(--ink-2);font-size:15px;line-height:1.55;margin:0 0 18px}
@media (max-width:900px){.f8-launch{grid-template-columns:1fr}}

315
fusion/fusion.js Normal file
View File

@@ -0,0 +1,315 @@
// Fusion AI, powered by HAL 8: the hub's consumer assistant.
// One control (Standard | Incognito); everything else is always on: chat and code, images and
// video, web search, a Python interpreter that runs in the visitor's browser, and voice.
// The browser never holds a key: it talks to HAL 8's public Fusion endpoint, which runs the
// request through HAL 8's headless pipeline under its own capped service account.
// Standard - hal8-auto; the conversation is kept in this browser (localStorage) until cleared.
// Incognito - a Venice private model; no conversation is kept, here or on HAL 8, and the
// pictures and clips it makes are deleted after a day.
const EYE = 'universe/logos/hal8-eye.png';
const LAUNCH = new URL('launch/', import.meta.url);
const FILM_V = '2';
const STORE = 'f8.chat.v1';
const VISITOR = 'f8.visitor';
const PYODIDE = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/';
const SUGGEST = {
standard: [['Ask', 'How do I stake AP3X?'], ['Code', 'A Python snippet that converts DFM to AP3X'],
['Image', 'The AP3X star rising over PRIME'], ['Video', 'A five-second flight through an asteroid belt']],
incognito: [['Ask', 'Explain AP3X tokenomics simply'], ['Image', 'A night sky over the Nexus world']],
};
const I = {
incog: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18"/><path d="M10.6 5.1A10.4 10.4 0 0 1 12 5c5 0 9 4.5 10 7-.4 1-1.2 2.3-2.4 3.5M6.2 6.3C4.2 7.6 2.7 9.6 2 12c1 2.5 5 7 10 7 1.7 0 3.3-.5 4.6-1.3"/><path d="M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>',
mic: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>',
send: '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>',
stop: '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
plus: '<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
copy: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.7"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3"/></svg>',
speak: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M11 5 6 9H3v6h3l5 4zM15.5 8.5a5 5 0 0 1 0 7M18.5 5.5a9 9 0 0 1 0 13"/></svg>',
dl: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 4v11M7 10l5 5 5-5M5 20h14"/></svg>',
};
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
// media and links: https only; paths HAL 8 returns relative to itself resolve against the endpoint
let ORIGIN = 'https://hal8.odyssey-works.io';
const safeUrl = u => {
if (/^\/(?!\/)/.test(u)) return ORIGIN + u;
if (/^https:\/\//i.test(u)) return u;
if (ORIGIN.startsWith('http://127.0.0.1') && /^http:\/\/127\.0\.0\.1[:/]/.test(u)) return u; // local testing only
return null;
};
const isVideo = u => /\.(mp4|webm|mov)(\?|#|$)/i.test(u) || /[?&](type|kind)=video/i.test(u);
// HAL 8's signed file links carry no extension: a HAL 8 file linked as "video" / "clip" is one
const isHal8File = u => u.startsWith(ORIGIN + '/api/v1/files/');
const namedVideo = (label, u) => isHal8File(u) && /^\s*(video|clip|watch)\b/i.test(label);
// ------------------------------------------------------------------ a small, safe markdown renderer
// Everything is escaped first; only code fences, inline code, emphasis, headings, lists, links,
// images and videos (https only) are turned back into markup.
export function renderMarkdown(src) {
const blocks = [];
let text = String(src ?? '').replace(/```([\w+-]*)[^\n]*\n([\s\S]*?)(```|$)/g, (_, lang, code) => {
blocks.push({ lang: (lang || '').toLowerCase(), code: code.replace(/\n$/, '') });
return `\u0000${blocks.length - 1}\u0000`;
});
text = esc(text);
const inline = s => s
.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, u) => { u = u.replace(/&amp;/g, '&'); const ok = safeUrl(u); if (!ok) return esc(alt);
return isVideo(ok) ? media(ok, 'video', alt) : media(ok, 'image', alt); })
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, t, u) => { u = u.replace(/&amp;/g, '&'); const ok = safeUrl(u); if (!ok) return t;
return isVideo(ok) || namedVideo(t, ok) ? media(ok, 'video', t) : `<a href="${esc(ok)}" target="_blank" rel="noopener nofollow">${t}</a>`; })
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>').replace(/(^|[\s(])\*([^*\n]+)\*/g, '$1<i>$2</i>');
const out = [];
let list = null;
const flush = () => { if (list) { out.push(`<${list.t}>${list.items.map(x => `<li>${inline(x)}</li>`).join('')}</${list.t}>`); list = null; } };
for (const raw of text.split('\n')) {
const line = raw.trimEnd();
const code = line.match(/^\u0000(\d+)\u0000$/);
if (code) { flush(); out.push(codeBlock(blocks[+code[1]])); continue; }
const h = line.match(/^(#{1,4})\s+(.*)$/);
if (h) { flush(); out.push(`<h4>${inline(h[2])}</h4>`); continue; }
const ul = line.match(/^\s*[-*•]\s+(.*)$/), ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
if (ul || ol) { const t = ul ? 'ul' : 'ol'; if (!list || list.t !== t) { flush(); list = { t, items: [] }; } list.items.push((ul || ol)[1]); continue; }
flush();
if (!line.trim()) continue;
out.push(`<p>${inline(line).replace(/\u0000(\d+)\u0000/g, (_, n) => codeBlock(blocks[+n]))}</p>`);
}
flush();
return out.join('');
}
function media(u, kind, label) {
const body = kind === 'video' ? `<video src="${esc(u)}" controls playsinline preload="metadata"></video>` : `<img src="${esc(u)}" alt="${esc(label || '')}" loading="lazy">`;
return `<span class="f8-media">${body}<span class="f8-media-bar"><span>${kind === 'video' ? 'Video' : 'Image'} &middot; HAL 8</span><a href="${esc(u)}" download target="_blank" rel="noopener" title="Download">${I.dl}</a></span></span>`;
}
function codeBlock(b) {
const runnable = ['python', 'py', ''].includes(b.lang) && /print|=|def |import /.test(b.code);
return `<div class="f8-code" data-lang="${esc(b.lang || 'code')}"><div class="f8-code-h"><span>${esc(b.lang || 'code')}</span><span class="f8-code-act">` +
(runnable ? '<button data-f8="run">&#9654; Run</button>' : '') + `<button data-f8="copy-code">${I.copy} Copy</button></span></div><pre>${esc(b.code)}</pre></div>`;
}
// ------------------------------------------------------------------ the widget
export function mount(root, opts = {}) {
const endpoint = opts.endpoint;
try { ORIGIN = new URL(endpoint).origin; } catch { /* keep HAL 8 */ }
const state = { open: false, mode: 'standard', msgs: { standard: load(), incognito: [] }, busy: false, ctrl: null, voiceTurn: false };
root.classList.add('f8-root');
root.innerHTML = `
<button class="f8-fab" aria-label="Ask Fusion AI" aria-expanded="false" aria-controls="f8-pane"><img src="${EYE}" alt="HAL 8"><span class="f8-fab-x" aria-hidden="true">&times;</span></button>
<div class="f8-pane" id="f8-pane" role="dialog" aria-label="Fusion AI" hidden>
<div class="f8-head">
<div><div class="f8-title serif">Fusion <span class="serif-i">AI</span></div><div class="f8-power"><img src="${EYE}" alt="">POWERED BY HAL 8</div></div>
<div class="f8-head-actions"><button data-f8="new" title="New chat" aria-label="New chat">${I.plus}</button><button data-f8="close" class="f8-x" title="Close" aria-label="Close">&times;</button></div>
</div>
<div class="f8-modebar"><div class="f8-pill" role="tablist" aria-label="Mode">
<button role="tab" data-mode="standard" aria-selected="true" class="on">Standard</button>
<button role="tab" data-mode="incognito" aria-selected="false">${I.incog} Incognito</button></div></div>
<div class="f8-incog-note">${I.incog} Incognito &middot; nothing is saved &middot; pictures vanish in a day</div>
<div class="f8-body" aria-live="polite"></div>
<div class="f8-compose">
<div class="f8-listen" hidden><span class="f8-wave"><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span><span class="f8-heard">Listening&hellip;</span></div>
<textarea rows="1" placeholder="Ask anything, or tap the mic" aria-label="Message Fusion AI"></textarea>
<button class="f8-mic" data-f8="mic" title="Speak" aria-label="Speak">${I.mic}</button>
<button class="f8-send" data-f8="send" title="Send" aria-label="Send">${I.send}</button>
</div>
</div>`;
const $ = s => root.querySelector(s);
const fab = $('.f8-fab'), pane = $('.f8-pane'), body = $('.f8-body'), ta = $('textarea'), sendBtn = $('.f8-send'), micBtn = $('.f8-mic');
// a HAL 8 file embedded as an image that will not decode is a clip: play it instead
body.addEventListener('error', e => {
const img = e.target;
if (img.tagName !== 'IMG' || !img.closest('.f8-media') || !isHal8File(img.src)) return;
const v = document.createElement('video');
Object.assign(v, { src: img.src, controls: true, playsInline: true, preload: 'metadata' });
img.replaceWith(v);
const tag = v.parentElement?.querySelector('.f8-media-bar span');
if (tag) tag.innerHTML = 'Video &middot; HAL 8';
}, true);
function load() { try { return JSON.parse(localStorage.getItem(STORE) || '[]').slice(-60); } catch { return []; } }
function save() { if (state.mode !== 'standard') return; try { localStorage.setItem(STORE, JSON.stringify(state.msgs.standard.slice(-60))); } catch { /* storage off */ } }
function visitor() {
try { let v = localStorage.getItem(VISITOR); if (!v) { v = crypto.randomUUID(); localStorage.setItem(VISITOR, v); } return v; } catch { return 'anon'; }
}
const msgs = () => state.msgs[state.mode];
function setOpen(o) {
state.open = o; pane.hidden = !o; root.classList.toggle('f8-open', o); fab.setAttribute('aria-expanded', String(o));
if (o) { draw(); setTimeout(() => ta.focus(), 30); } else { stopVoice(); speechSynthesis?.cancel?.(); }
}
function setMode(m) {
if (m === state.mode) return;
state.ctrl?.abort();
if (state.mode === 'incognito') state.msgs.incognito = []; // leaving incognito forgets it
state.mode = m; root.classList.toggle('f8-incognito', m === 'incognito');
root.querySelectorAll('.f8-pill button').forEach(b => { const on = b.dataset.mode === m; b.classList.toggle('on', on); b.setAttribute('aria-selected', String(on)); });
draw();
}
function draw() {
const list = msgs();
if (!list.length) {
const s = SUGGEST[state.mode];
body.innerHTML = `<div class="f8-empty"><p class="serif">${state.mode === 'incognito' ? "You're incognito. <span class=\"serif-i\">Nothing you say here is kept.</span>" : "Hi, I'm Fusion AI. <span class=\"serif-i\">Ask, code, create or just talk.</span>"}</p>
<div class="f8-sugg">${s.map(([k, t]) => `<button data-f8="suggest"><b>${k}</b>${esc(t)}</button>`).join('')}</div>
${state.mode === 'standard' ? '<button class="f8-watch" data-f8="watch"><i></i>Watch the launch film<span>0:32</span></button>' : ''}</div>`;
return;
}
body.innerHTML = list.map((m, i) => bubble(m, i)).join('');
body.scrollTop = body.scrollHeight;
}
function bubble(m, i) {
if (m.role === 'user') return `<div class="f8-u">${esc(m.content)}</div>`;
if (m.kind === 'limit') return `<div class="f8-a f8-limit">${esc(m.content)}</div>`;
const tools = m.content && !m.pending ? `<div class="f8-meta"><button data-f8="copy" data-i="${i}" title="Copy">${I.copy}</button><button data-f8="speak" data-i="${i}" title="Read aloud">${I.speak}</button></div>` : '';
const status = m.pending && m.status ? `<div class="f8-status"><span class="f8-dots"><i></i><i></i><i></i></span>${esc(m.status)}</div>` : '';
const html = m.content ? renderMarkdown(m.content) : '';
return `<div class="f8-a${m.pending && !m.content ? ' f8-thinking' : ''}">${html}${status || (m.pending && !m.content ? '<span class="f8-dots"><i></i><i></i><i></i></span>' : '')}${tools}</div>`;
}
function redrawLast() {
const list = msgs(), el = body.lastElementChild;
if (!el || !list.length) return draw();
const tmp = document.createElement('div'); tmp.innerHTML = bubble(list[list.length - 1], list.length - 1);
el.replaceWith(tmp.firstElementChild); body.scrollTop = body.scrollHeight;
}
// ---------------------------------------------------------------- sending, streamed
async function send(text) {
text = (text || '').trim();
if (!text || state.busy) return;
const list = msgs();
list.push({ role: 'user', content: text });
const reply = { role: 'assistant', content: '', pending: true };
list.push(reply);
ta.value = ''; autoGrow(); state.busy = true; sendBtn.innerHTML = I.stop; sendBtn.title = 'Stop'; draw();
const ctrl = new AbortController(); state.ctrl = ctrl;
const history = list.filter(m => !m.pending && m.kind !== 'limit').slice(-16).map(m => ({ role: m.role, content: m.content }));
try {
const r = await fetch(endpoint, {
method: 'POST', signal: ctrl.signal, headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({ mode: state.mode, messages: history, visitor: state.mode === 'incognito' ? undefined : visitor(), stream: true }),
});
if (!r.ok) {
let e = {}; try { e = (await r.json()).error || {}; } catch { /* not json */ }
reply.pending = false;
if (r.status === 429) { reply.kind = 'limit'; reply.content = e.message || "That's today's allowance for Fusion AI. It resets at 00:00 UTC; see you then."; }
else reply.content = e.message || "I couldn't reach HAL 8 just now. Try again in a moment.";
return;
}
const ct = r.headers.get('content-type') || '';
if (ct.includes('text/event-stream') && r.body) {
const rd = r.body.getReader(), dec = new TextDecoder(); let buf = '';
for (;;) {
const { value, done } = await rd.read(); if (done) break;
buf += dec.decode(value, { stream: true });
let k;
while ((k = buf.indexOf('\n\n')) >= 0) {
const evt = buf.slice(0, k); buf = buf.slice(k + 2);
for (const ln of evt.split('\n')) {
if (!ln.startsWith('data:')) continue;
const d = ln.slice(5).trim(); if (!d || d === '[DONE]') continue;
let j; try { j = JSON.parse(d); } catch { continue; }
if (j.error) { reply.kind = j.error.code && /limit|budget/.test(j.error.code) ? 'limit' : ''; reply.content = j.error.message || 'Something went wrong.'; continue; }
if (j.status) reply.status = j.status; // e.g. "Searching the web", "Rendering video"
const delta = j.choices?.[0]?.delta?.content ?? j.choices?.[0]?.message?.content ?? '';
if (delta) reply.content += delta;
}
redrawLast();
}
}
} else {
const j = await r.json();
reply.content = j.choices?.[0]?.message?.content || j.text || '';
}
if (!reply.content) reply.content = 'Hmm, an empty answer. Try asking again.';
} catch (e) {
if (e.name !== 'AbortError') reply.content = reply.content || "I couldn't reach HAL 8 just now. Try again in a moment.";
} finally {
reply.pending = false; reply.status = ''; state.busy = false; state.ctrl = null; sendBtn.innerHTML = I.send; sendBtn.title = 'Send';
draw(); save();
if (state.voiceTurn && reply.content && reply.kind !== 'limit') speak(reply.content);
state.voiceTurn = false;
}
}
// ---------------------------------------------------------------- voice: speak in, read out
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
let rec = null;
function startVoice() {
if (!SR) { toast('Voice needs Chrome, Edge or Safari on this device.'); return; }
speechSynthesis?.cancel?.();
rec = new SR(); rec.lang = navigator.language || 'en-US'; rec.interimResults = true; rec.continuous = false;
let finalText = '';
const heard = $('.f8-heard'); $('.f8-listen').hidden = false; root.classList.add('f8-voice');
rec.onresult = ev => { let interim = ''; for (let i = ev.resultIndex; i < ev.results.length; i++) { const t = ev.results[i][0].transcript; if (ev.results[i].isFinal) finalText += t; else interim += t; }
heard.innerHTML = `Listening&hellip; <em>${esc((finalText + ' ' + interim).trim())}</em>`; };
rec.onerror = ev => { if (ev.error === 'not-allowed') toast('Allow the microphone to talk to Fusion AI.'); };
rec.onend = () => { stopVoice(); if (finalText.trim()) { state.voiceTurn = true; send(finalText); } };
rec.start();
}
function stopVoice() { root.classList.remove('f8-voice'); $('.f8-listen').hidden = true; $('.f8-heard').innerHTML = 'Listening&hellip;'; if (rec) { try { rec.stop(); } catch { /* already stopped */ } rec = null; } }
function speak(md) {
if (!('speechSynthesis' in window)) return;
const plain = md.replace(/```[\s\S]*?```/g, ' (code shown on screen) ').replace(/!?\[([^\]]*)\]\([^)]+\)/g, '$1').replace(/[#*`>_]/g, '').slice(0, 1800);
speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(plain); u.lang = navigator.language || 'en-US'; speechSynthesis.speak(u);
}
// ---------------------------------------------------------------- the code interpreter (Pyodide, in this browser)
let py = null;
async function runPython(btn) {
const box = btn.closest('.f8-code'); const code = box.querySelector('pre').textContent;
let out = box.querySelector('.f8-out'); if (!out) { out = document.createElement('div'); out.className = 'f8-out'; box.appendChild(out); }
out.innerHTML = '<span>output</span>Starting Python&hellip;';
try {
if (!py) py = (async () => { await new Promise((res, rej) => { const s = document.createElement('script'); s.src = PYODIDE + 'pyodide.js'; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); return window.loadPyodide({ indexURL: PYODIDE }); })();
const p = await py; let buf = '';
p.setStdout({ batched: s => { buf += s + '\n'; } }); p.setStderr({ batched: s => { buf += s + '\n'; } });
await p.loadPackagesFromImports(code);
const res = await p.runPythonAsync(code);
if (res !== undefined && res !== null && !buf) buf = String(res);
out.innerHTML = `<span>output</span>${esc(buf.trim() || '(no output)')}`;
} catch (e) { out.innerHTML = `<span>error</span>${esc(String(e.message || e).split('\n').slice(-3).join('\n'))}`; }
}
// ---------------------------------------------------------------- events
function toast(t) { const el = document.createElement('div'); el.className = 'f8-toast'; el.textContent = t; pane.appendChild(el); setTimeout(() => el.remove(), 3200); }
function autoGrow() { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 120) + 'px'; }
fab.addEventListener('click', () => setOpen(!state.open));
ta.addEventListener('input', autoGrow);
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(ta.value); } });
root.addEventListener('keydown', e => { if (e.key === 'Escape' && state.open) setOpen(false); });
root.addEventListener('click', e => {
const b = e.target.closest('[data-f8],[data-mode]'); if (!b) return;
if (b.dataset.mode) return setMode(b.dataset.mode);
const act = b.dataset.f8, list = msgs();
if (act === 'close') setOpen(false);
else if (act === 'new') { state.ctrl?.abort(); state.msgs[state.mode] = []; if (state.mode === 'standard') { try { localStorage.removeItem(STORE); } catch { /* */ } } draw(); }
else if (act === 'send') { if (state.busy) state.ctrl?.abort(); else send(ta.value); }
else if (act === 'mic') { if (root.classList.contains('f8-voice')) stopVoice(); else startVoice(); }
else if (act === 'suggest') send(b.childNodes[1]?.textContent || b.textContent);
else if (act === 'copy') navigator.clipboard?.writeText(list[+b.dataset.i]?.content || '');
else if (act === 'speak') speak(list[+b.dataset.i]?.content || '');
else if (act === 'copy-code') navigator.clipboard?.writeText(b.closest('.f8-code').querySelector('pre').textContent);
else if (act === 'run') runPython(b);
else if (act === 'watch') watchFilm();
});
function watchFilm() {
const box = document.createElement('div');
box.className = 'f8-film';
box.setAttribute('role', 'dialog');
box.setAttribute('aria-label', 'Fusion AI launch film');
box.innerHTML = `<div class="f8-film-frame"><video controls autoplay playsinline preload="auto"
src="${new URL('fusion-ai-launch.mp4?v=' + FILM_V, LAUNCH)}" poster="${new URL('poster.jpg?v=' + FILM_V, LAUNCH)}">
<track kind="captions" srclang="en" label="English" src="${new URL('fusion-ai-launch.vtt?v=' + FILM_V, LAUNCH)}"></video>
<button class="f8-film-x" aria-label="Close">&times;</button></div>`;
const close = () => { box.querySelector('video')?.pause(); box.remove(); document.removeEventListener('keydown', onKey); };
const onKey = e => { if (e.key === 'Escape') close(); };
box.addEventListener('click', e => { if (e.target === box || e.target.closest('.f8-film-x')) close(); });
document.addEventListener('keydown', onKey);
document.body.appendChild(box);
}
const onExternalOpen = () => { setOpen(true); };
window.addEventListener('fusion:open', onExternalOpen);
return {
open: () => setOpen(true),
destroy() { state.ctrl?.abort(); stopVoice(); window.removeEventListener('fusion:open', onExternalOpen); root.innerHTML = ''; },
};
}

Binary file not shown.

View File

@@ -0,0 +1,45 @@
WEBVTT
1
00:00:01.600 --> 00:00:03.700
Across three chains…
2
00:00:06.100 --> 00:00:08.800
…one intelligence was always watching.
3
00:00:12.200 --> 00:00:13.600
Ask it anything.
4
00:00:14.300 --> 00:00:15.500
Write code.
5
00:00:16.350 --> 00:00:17.900
Create worlds.
6
00:00:19.600 --> 00:00:21.100
Or just… talk.
7
00:00:22.100 --> 00:00:23.900
And when it’s just you…
8
00:00:24.000 --> 00:00:25.300
…nothing is kept.
9
00:00:26.450 --> 00:00:27.700
Fusion AI.
10
00:00:27.950 --> 00:00:29.100
Powered by HAL 8.
11
00:00:29.450 --> 00:00:31.500
Now live.

BIN
fusion/launch/poster.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

File diff suppressed because one or more lines are too long

89
tools/fusion_inject.py Normal file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Put Fusion AI (powered by HAL 8) into the hub, replacing the old assistant pane.
python3 tools/fusion_inject.py
The widget itself is fusion/fusion.js (served as a file, loaded on demand) and fusion/fusion.css
(inlined into the bundle). The old block, from its "FUSION AI — assistant pane" banner to the end of
the FusionAI component, is replaced by a small component between /*F8-BEGIN*/ and /*F8-END*/;
re-running replaces that block and the style block again, so the script is idempotent.
The endpoint is HAL 8's public Fusion route; window.FUSION_AI_ENDPOINT still overrides it.
"""
import hashlib
import re
import subprocess
import sys
from pathlib import Path
SITE = Path(__file__).resolve().parents[1]
ENDPOINT = "https://hal8.odyssey-works.io/v1/public/fusion/chat"
tool = SITE / "tools" / "bundle.py"
tmp = SITE / "tools" / "_page.html"
subprocess.run([sys.executable, str(tool), "decode", str(SITE / "index.html"), str(tmp)], check=True, capture_output=True)
page = tmp.read_text("utf-8")
VER = hashlib.sha1((SITE / "fusion" / "fusion.js").read_bytes()).hexdigest()[:10]
FILM = SITE / "fusion" / "launch" / "fusion-ai-launch.mp4"
FILM_V = hashlib.sha1(FILM.read_bytes()).hexdigest()[:10] if FILM.exists() else "0"
COMPONENT = f"""/*F8-BEGIN*/
// ============================================================================
// FUSION AI — powered by HAL 8 (fusion/fusion.js; the key lives on HAL 8, never here)
// ============================================================================
function FusionAI() {{
const ref = useRef(null);
useEffect(() => {{
let api = null, dead = false;
import("./fusion/fusion.js?v={VER}")
.then(m => {{ if (!dead) api = m.mount(ref.current, {{ endpoint: window.FUSION_AI_ENDPOINT || "{ENDPOINT}" }}); }})
.catch(e => console.warn("[Fusion AI]", e));
return () => {{ dead = true; if (api) api.destroy(); }};
}}, []);
return MG("div", {{ ref }});
}}
// the launch film, on the HAL 8 page (fusion/launch/; "Talk to Fusion AI" opens the widget)
function FusionLaunchCard() {{
const film = "fusion/launch/fusion-ai-launch";
return MG("div", {{ className: "f8-launch" }},
MG("div", {{ className: "f8-launch-media" }},
MG("video", {{ src: film + ".mp4?v={FILM_V}", poster: "fusion/launch/poster.jpg?v={FILM_V}", controls: true, playsInline: true, preload: "none" }},
MG("track", {{ kind: "captions", src: film + ".vtt?v={FILM_V}", srcLang: "en", label: "English" }}))),
MG("div", {{ className: "f8-launch-copy" }},
MG("div", {{ className: "f8-launch-kicker" }}, "NEW · FUSION AI"),
MG("h3", null, "Meet Fusion AI. ", MG("span", {{ className: "serif-i" }}, "Powered by HAL 8.")),
MG("p", null, "The assistant on this site. Ask anything, write and run code, make images and short video, or just talk. Standard keeps your chat in this browser; Incognito keeps nothing."),
MG("div", {{ className: "h8-cta" }},
MG("button", {{ className: "mgx-btn pri", onClick: () => window.dispatchEvent(new CustomEvent("fusion:open")) }}, "Talk to Fusion AI"),
MG("a", {{ className: "mgx-btn", href: film + ".mp4", download: "fusion-ai-launch.mp4" }}, "Download the film"))));
}}
/*F8-END*/
"""
if "/*F8-BEGIN*/" in page:
i = page.index("/*F8-BEGIN*/")
j = page.index("/*F8-END*/\n", i) + len("/*F8-END*/\n")
else:
banner = page.index("// FUSION AI — assistant pane")
i = page.rfind("// ====", 0, banner)
f = page.index("function FusionAI()", banner)
j = re.compile(r"\n}\n").search(page, f).end()
old = page[i:j]
assert old.count("function ") == 1 and "SYSTEM_PROMPT" in old and "Powered by Claude" in old, "unexpected old block"
page = page[:i] + COMPONENT + page[j:]
assert "Powered by Claude" not in page and page.count("function FusionAI()") == 1
# the card sits in the HAL 8 section, right above "Inside the harness"
HOOK = 'MG("div", { className: "mg-subhead" }, "Inside the harness"),'
if "MG(FusionLaunchCard, null)" not in page:
assert page.count(HOOK) == 1, "HAL 8 section hook not found"
page = page.replace(HOOK, "MG(FusionLaunchCard, null),\n " + HOOK)
css = (SITE / "fusion" / "fusion.css").read_text("utf-8")
page = re.sub(r"<style>/\*f8-css-begin\*/.*?/\*f8-css-end\*/</style>\n", "", page, flags=re.S)
k = page.rfind("</head>")
page = page[:k] + "<style>/*f8-css-begin*/" + css + "/*f8-css-end*/</style>\n" + page[k:]
tmp.write_text(page, "utf-8")
r = subprocess.run([sys.executable, str(tool), "encode", str(SITE / "index.html"), str(tmp), str(SITE / "index.html")], capture_output=True, text=True)
tmp.unlink()
print(r.stdout.strip() or r.stderr.strip())
print(f"fusion.js v{VER}")

File diff suppressed because one or more lines are too long