Compare commits
17 Commits
feat/apex-
...
feat/marke
| Author | SHA1 | Date | |
|---|---|---|---|
| 9276c7cd75 | |||
| de5b01cd01 | |||
| 2373ea5299 | |||
| a3960a5bf9 | |||
| 7347a647e8 | |||
| 78f1df4934 | |||
| 16126e9203 | |||
| 830d021f94 | |||
| d7b0434e58 | |||
| dfa72eec45 | |||
| c97d524025 | |||
| b9b9fe06d8 | |||
| 23b05a72da | |||
| b2105ffa46 | |||
| 716615a1d2 | |||
| f50457b1c1 | |||
| 103c7903ff |
38
README.md
38
README.md
@@ -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
93
fusion/fusion.css
Normal 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
315
fusion/fusion.js
Normal 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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(/&/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(/&/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'} · 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">▶ 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">×</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">×</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 · nothing is saved · 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…</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 · 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… <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…'; 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…';
|
||||
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">×</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 = ''; },
|
||||
};
|
||||
}
|
||||
BIN
fusion/launch/fusion-ai-launch.mp4
Normal file
BIN
fusion/launch/fusion-ai-launch.mp4
Normal file
Binary file not shown.
45
fusion/launch/fusion-ai-launch.vtt
Normal file
45
fusion/launch/fusion-ai-launch.vtt
Normal 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
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
89
tools/fusion_inject.py
Normal 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
Reference in New Issue
Block a user