/install takes a reader from three bare Ubuntu machines to a first answer: what each machine needs and the commands that check it, the release fetched and its checksum checked, the eight setup screens, the system's state and the console, and a first request with a key from the account page. Every screen is from an install of v0.14.9 on three fresh VMs, recorded with tmux and kept as terminal frames: the page shows them as text, and plays a session back when asked, fetching it compressed from /install/cast/. The administrator's password and the API key were masked before the recording was kept, and a test fails if either shape reaches the page. /about gains what llmgw now does beyond routing, and its install steps are today's: the setup screens and node add, not onboard-node.sh and a model file, with a link into the guide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
368 lines
20 KiB
HTML
368 lines
20 KiB
HTML
{{define "install"}}{{if not .Site}}<!doctype html>
|
||
<html lang="{{lang}}"><head>
|
||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<meta name="description" content="{{t "install.meta.description"}}">
|
||
{{end}}<title>{{t "install.title"}} · llmgw</title>
|
||
{{template "sitestyle" .}}
|
||
<style>
|
||
/* the recorded terminal */
|
||
.tty{margin-top:18px;background:#0f1416;border:1px solid #22302f;border-radius:10px;overflow:hidden;min-width:0}
|
||
.tty-bar{display:flex;align-items:center;gap:12px;padding:9px 12px;background:#161e20;border-bottom:1px solid #22302f}
|
||
.tty-dots{display:flex;gap:6px}
|
||
.tty-dots i{width:11px;height:11px;border-radius:50%;background:#33403f}
|
||
.tty-title{flex:1;font:500 .75rem/1 var(--mono);color:#7f9a97;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.tty-play{font:600 .78rem/1 var(--body);padding:7px 12px;border-radius:6px;border:1px solid #2d4442;background:#1c2b2a;color:#dceae8;cursor:pointer}
|
||
.tty-play:hover{border-color:#7f9a97}
|
||
.tty-screen{margin:0;padding:12px 14px;overflow-x:auto;color:#d6d8d6;font:400 clamp(7px,1.18vw,13px)/1.22 var(--mono);white-space:pre;tab-size:8}
|
||
.tty-progress{height:3px;background:#1c2b2a}
|
||
.tty-progress i{display:block;height:3px;width:0;background:var(--fiber);transition:width .2s linear}
|
||
.tty figcaption,.shot figcaption{padding:10px 14px 12px;font-size:.85rem;color:#9fb3b1;border-top:1px solid #22302f}
|
||
.shot{margin-top:18px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--surface)}
|
||
.shot img{display:block;width:100%;height:auto}
|
||
.shot figcaption{color:var(--muted);border-top:1px solid var(--line)}
|
||
.shots{display:grid;grid-template-columns:1fr;gap:18px}
|
||
.full{margin-top:28px}
|
||
.req td:first-child{white-space:nowrap}
|
||
.note-box{margin-top:18px;padding:14px 16px;border-left:3px solid var(--om1);background:var(--om1-soft);border-radius:0 8px 8px 0;font-size:.92rem}
|
||
.toc{display:flex;flex-wrap:wrap;gap:10px;margin-top:28px;padding:0;list-style:none}
|
||
.toc a{display:inline-block;padding:8px 12px;border:1px solid var(--line);border-radius:999px;text-decoration:none;color:var(--ink);font-size:.88rem;background:var(--surface)}
|
||
.toc a b{color:var(--fiber);margin-right:6px}
|
||
.facts-row{display:flex;flex-wrap:wrap;gap:10px 28px;margin-top:26px;color:var(--muted);font-size:.9rem}
|
||
.facts-row b{display:block;font:800 1.5rem/1.1 var(--display);font-variation-settings:"wdth" 125;color:var(--ink)}
|
||
/* the setup screens, one at a time */
|
||
.screens input{position:absolute;opacity:0;pointer-events:none}
|
||
.screen-tabs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
||
.screen-tabs label{cursor:pointer;padding:8px 11px;border:1px solid var(--line);border-radius:8px;font:500 .84rem/1 var(--body);color:var(--muted);background:var(--surface)}
|
||
.screen-tabs label b{color:var(--fiber);margin-right:5px}
|
||
.screen{display:none}
|
||
.screen .what{margin-top:16px;max-width:52em;color:var(--muted)}
|
||
.screen .what strong{color:var(--ink)}
|
||
#sc1:checked~.screen-tabs label[for="sc1"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc1:checked~.sc1{display:block}
|
||
#sc2:checked~.screen-tabs label[for="sc2"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc2:checked~.sc2{display:block}
|
||
#sc3:checked~.screen-tabs label[for="sc3"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc3:checked~.sc3{display:block}
|
||
#sc4:checked~.screen-tabs label[for="sc4"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc4:checked~.sc4{display:block}
|
||
#sc5:checked~.screen-tabs label[for="sc5"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc5:checked~.sc5{display:block}
|
||
#sc6:checked~.screen-tabs label[for="sc6"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc6:checked~.sc6{display:block}
|
||
#sc7:checked~.screen-tabs label[for="sc7"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc7:checked~.sc7{display:block}
|
||
#sc8:checked~.screen-tabs label[for="sc8"]{color:var(--ink);border-color:var(--fiber);background:var(--fiber-soft)}
|
||
#sc8:checked~.sc8{display:block}
|
||
.trouble td{vertical-align:top}
|
||
.trouble td:first-child{white-space:normal;width:38%}
|
||
@media (max-width:860px){.shots{grid-template-columns:1fr}}
|
||
@media (prefers-reduced-motion:reduce){.tty-progress i{transition:none}}
|
||
</style>
|
||
{{if not .Site}}</head><body>{{end}}
|
||
|
||
<div class="wrap">
|
||
<header class="bar">
|
||
<a class="mark" href="{{.AboutURL}}" aria-label="{{t "about.home_label"}}">
|
||
<svg width="26" height="26" viewBox="0 0 26 26" aria-hidden="true">
|
||
<rect x="1" y="1" width="24" height="24" rx="6" fill="none" stroke="currentColor" stroke-width="2"/>
|
||
<path d="M7 9h12M7 13h12M7 17h12" stroke="var(--fiber)" stroke-width="2" stroke-linecap="round"/>
|
||
</svg>
|
||
llmgw
|
||
</a>
|
||
<nav aria-label="{{t "install.nav.label"}}">
|
||
<a href="#before">{{t "install.nav.before"}}</a>
|
||
<a href="#binary">{{t "install.nav.binary"}}</a>
|
||
<a href="#setup">{{t "install.nav.setup"}}</a>
|
||
<a href="#check">{{t "install.nav.check"}}</a>
|
||
<a href="#use">{{t "install.nav.use"}}</a>
|
||
<a href="#trouble">{{t "install.nav.trouble"}}</a>
|
||
<a href="{{.AboutURL}}">{{t "install.nav.about"}}</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<div class="hero" id="top" style="grid-template-columns:1fr">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.hero.eyebrow" .Recorded}}</p>
|
||
<h1 style="margin-top:14px">{{th "install.hero.title"}}</h1>
|
||
<p class="lede" style="max-width:46em">{{t "install.hero.lede"}}</p>
|
||
<div class="facts-row">
|
||
<span><b>3</b>{{t "install.hero.machines"}}</span>
|
||
<span><b>4</b>{{t "install.hero.steps"}}</span>
|
||
<span><b>{{t "install.hero.time_value"}}</b>{{t "install.hero.time"}}</span>
|
||
<span><b>0</b>{{t "install.hero.hidden"}}</span>
|
||
</div>
|
||
<ol class="toc">
|
||
<li><a href="#before"><b>0</b>{{t "install.nav.before"}}</a></li>
|
||
<li><a href="#binary"><b>1</b>{{t "install.nav.binary"}}</a></li>
|
||
<li><a href="#setup"><b>2</b>{{t "install.nav.setup"}}</a></li>
|
||
<li><a href="#check"><b>3</b>{{t "install.nav.check"}}</a></li>
|
||
<li><a href="#use"><b>4</b>{{t "install.nav.use"}}</a></li>
|
||
<li><a href="#next"><b>→</b>{{t "install.nav.next"}}</a></li>
|
||
</ol>
|
||
</div>
|
||
</div>
|
||
|
||
<section id="before">
|
||
<div class="sec-head">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.step" "0"}}</p>
|
||
<h2 style="margin-top:8px">{{t "install.before.title"}}</h2>
|
||
<p class="intro">{{t "install.before.intro"}}</p>
|
||
</div>
|
||
<div>
|
||
<div class="dl-wrap">
|
||
<table class="dl req">
|
||
<tbody>
|
||
<tr><td>{{t "install.req.machines"}}</td><td>{{t "install.req.machines.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.system"}}</td><td>{{t "install.req.system.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.size"}}</td><td>{{t "install.req.size.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.clock"}}</td><td>{{t "install.req.clock.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.network"}}</td><td>{{th "install.req.network.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.access"}}</td><td>{{th "install.req.access.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.names"}}</td><td>{{t "install.req.names.text"}}</td></tr>
|
||
<tr><td>{{t "install.req.gpus"}}</td><td>{{t "install.req.gpus.text"}}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="note-box">{{th "install.req.memory_note"}}</p>
|
||
<p style="margin-top:18px;color:var(--muted)">{{t "install.before.commands"}}</p>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>grep PRETTY_NAME /etc/os-release; uname -m; nproc; free -h | grep Mem; df -h /
|
||
timedatectl | grep synchronized; curl -sI https://gitea.mynodes.xyz | head -1
|
||
ping -c1 <second machine>; ping -c1 <third machine>
|
||
sudo -v && echo "sudo works"
|
||
sudo hostnamectl set-hostname llm-1 <span class="c"># a name of its own, on each machine</span></code></pre></div>
|
||
</div>
|
||
</div>
|
||
{{template "tty" (.Cast "0-prereq")}}
|
||
</section>
|
||
|
||
<section id="binary">
|
||
<div class="sec-head">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.step" "1"}}</p>
|
||
<h2 style="margin-top:8px">{{t "install.binary.title"}}</h2>
|
||
<p class="intro">{{t "install.binary.intro"}}</p>
|
||
</div>
|
||
<div>
|
||
<p style="color:var(--muted)">{{th "install.binary.text"}}</p>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>VERSION=$(curl -fsSL https://gitea.mynodes.xyz/api/v1/repos/ilker/llmgw/releases/latest \
|
||
| grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||
U=https://gitea.mynodes.xyz/ilker/llmgw/releases/download/$VERSION
|
||
curl -fsSLO $U/llmgw-$VERSION-linux-amd64 <span class="c"># -linux-arm64 on ARM</span>
|
||
curl -fsSL $U/checksums.txt | sha256sum -c --ignore-missing
|
||
sudo install -m0755 llmgw-$VERSION-linux-amd64 /usr/local/bin/llmgw
|
||
llmgw --version</code></pre></div>
|
||
</div>
|
||
</div>
|
||
{{template "tty" (.Cast "1-binary")}}
|
||
</section>
|
||
|
||
<section id="setup">
|
||
<div class="sec-head">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.step" "2"}}</p>
|
||
<h2 style="margin-top:8px">{{t "install.setup.title"}}</h2>
|
||
<p class="intro">{{t "install.setup.intro"}}</p>
|
||
</div>
|
||
<div>
|
||
<p style="color:var(--muted)">{{t "install.setup.text"}}</p>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw</code></pre></div>
|
||
<p style="margin-top:14px;color:var(--muted);font-size:.9rem">{{t "install.setup.keys"}}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="screens full">
|
||
<input type="radio" name="screen" id="sc1" checked><input type="radio" name="screen" id="sc2"><input type="radio" name="screen" id="sc3"><input type="radio" name="screen" id="sc4">
|
||
<input type="radio" name="screen" id="sc5"><input type="radio" name="screen" id="sc6"><input type="radio" name="screen" id="sc7"><input type="radio" name="screen" id="sc8">
|
||
<div class="screen-tabs" role="presentation">
|
||
<label for="sc1"><b>2.1</b>{{t "install.sc1.tab"}}</label>
|
||
<label for="sc2"><b>2.2</b>{{t "install.sc2.tab"}}</label>
|
||
<label for="sc3"><b>2.3</b>{{t "install.sc3.tab"}}</label>
|
||
<label for="sc4"><b>2.4</b>{{t "install.sc4.tab"}}</label>
|
||
<label for="sc5"><b>2.5</b>{{t "install.sc5.tab"}}</label>
|
||
<label for="sc6"><b>2.6</b>{{t "install.sc6.tab"}}</label>
|
||
<label for="sc7"><b>2.7</b>{{t "install.sc7.tab"}}</label>
|
||
<label for="sc8"><b>2.8</b>{{t "install.sc8.tab"}}</label>
|
||
</div>
|
||
<div class="screen sc1">{{template "still" (.Shot "setup-start" "2.1")}}<p class="what">{{th "install.sc1.text"}}</p></div>
|
||
<div class="screen sc2">{{template "still" (.Shot "setup-choices" "2.2")}}<p class="what">{{th "install.sc2.text"}}</p></div>
|
||
<div class="screen sc3">{{template "still" (.Shot "setup-admin" "2.3")}}<p class="what">{{th "install.sc3.text"}}</p></div>
|
||
<div class="screen sc4">{{template "still" (.Shot "setup-machines" "2.4")}}<p class="what">{{th "install.sc4.text"}}</p></div>
|
||
<div class="screen sc5">{{template "still" (.Shot "setup-checks" "2.5")}}<p class="what">{{th "install.sc5.text"}}</p></div>
|
||
<div class="screen sc6">{{template "still" (.Shot "setup-plan" "2.6")}}<p class="what">{{th "install.sc6.text"}}</p></div>
|
||
<div class="screen sc7">{{template "still" (.Shot "setup-install" "2.7")}}<p class="what">{{th "install.sc7.text"}}</p></div>
|
||
<div class="screen sc8">{{template "still" (.Shot "setup-done" "2.8")}}<p class="what">{{th "install.sc8.text"}}</p></div>
|
||
</div>
|
||
|
||
<div class="full">
|
||
<h3>{{t "install.setup.whole"}}</h3>
|
||
{{template "tty" (.Cast "2-setup")}}
|
||
</div>
|
||
|
||
<div class="full sec-head">
|
||
<div><h3>{{t "install.setup.script.title"}}</h3><p class="intro">{{t "install.setup.script.intro"}}</p></div>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo --preserve-env=SSH_AUTH_SOCK llmgw server init \
|
||
--admin you@example.com \
|
||
--patroni <user>@<second machine> \
|
||
--patroni <user>@<third machine> \
|
||
--standbys
|
||
|
||
sudo llmgw server init --admin you@example.com <span class="c"># one machine</span></code></pre></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="check">
|
||
<div class="sec-head">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.step" "3"}}</p>
|
||
<h2 style="margin-top:8px">{{t "install.check.title"}}</h2>
|
||
<p class="intro">{{t "install.check.intro"}}</p>
|
||
</div>
|
||
<div>
|
||
<p style="color:var(--muted)">{{th "install.check.text"}}</p>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>sudo llmgw status
|
||
sudo llmgw <span class="c"># the console</span></code></pre></div>
|
||
</div>
|
||
</div>
|
||
{{template "tty" (.Cast "3-check")}}
|
||
<div class="shots">
|
||
<figure class="shot"><img src="{{.Img "login.png"}}" alt="{{t "install.shot.login"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.login"}}</figcaption></figure>
|
||
<figure class="shot"><img src="{{.Img "admin.png"}}" alt="{{t "install.shot.admin"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.admin"}}</figcaption></figure>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="use">
|
||
<div class="sec-head">
|
||
<div>
|
||
<p class="eyebrow">{{t "install.step" "4"}}</p>
|
||
<h2 style="margin-top:8px">{{t "install.use.title"}}</h2>
|
||
<p class="intro">{{t "install.use.intro"}}</p>
|
||
</div>
|
||
<div>
|
||
<p style="color:var(--muted)">{{th "install.use.text"}}</p>
|
||
<div class="code"><button class="copy" type="button">{{t "about.copy"}}</button><pre><code>export LLMGW_API_KEY=sk-llmgw-… <span class="c"># from the account page</span>
|
||
curl http://<first machine>:8080/v1/chat/completions \
|
||
-H "Authorization: Bearer $LLMGW_API_KEY" \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"model": "qwen-0.5b", "messages": [{"role": "user", "content": "hello"}]}'</code></pre></div>
|
||
</div>
|
||
</div>
|
||
<div class="shots">
|
||
<figure class="shot"><img src="{{.Img "newkey.png"}}" alt="{{t "install.shot.newkey"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.newkey"}}</figcaption></figure>
|
||
<figure class="shot"><img src="{{.Img "chat.png"}}" alt="{{t "install.shot.chat"}}" loading="lazy" width="1280" height="650"><figcaption>{{t "install.shot.chat"}}</figcaption></figure>
|
||
</div>
|
||
{{template "tty" (.Cast "4-request")}}
|
||
</section>
|
||
|
||
<section id="next">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>{{t "install.next.title"}}</h2>
|
||
<p class="intro">{{t "install.next.intro"}}</p>
|
||
</div>
|
||
<div class="facts">
|
||
<div class="fact"><h3>{{t "install.next.nodes.title"}}</h3><p>{{th "install.next.nodes.text"}}</p></div>
|
||
<div class="fact"><h3>{{t "install.next.models.title"}}</h3><p>{{th "install.next.models.text"}}</p></div>
|
||
<div class="fact"><h3>{{t "install.next.people.title"}}</h3><p>{{th "install.next.people.text"}}</p></div>
|
||
<div class="fact"><h3>{{t "install.next.https.title"}}</h3><p>{{th "install.next.https.text" .AboutURL}}</p></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="trouble">
|
||
<div class="sec-head">
|
||
<div>
|
||
<h2>{{t "install.trouble.title"}}</h2>
|
||
<p class="intro">{{t "install.trouble.intro"}}</p>
|
||
</div>
|
||
<div class="dl-wrap">
|
||
<table class="dl trouble">
|
||
<thead><tr><th>{{t "install.trouble.see"}}</th><th>{{t "install.trouble.do"}}</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>{{th "install.trouble.starter.see"}}</td><td>{{th "install.trouble.starter.do"}}</td></tr>
|
||
<tr><td>{{th "install.trouble.judge.see"}}</td><td>{{th "install.trouble.judge.do"}}</td></tr>
|
||
<tr><td>{{th "install.trouble.ssh.see"}}</td><td>{{th "install.trouble.ssh.do"}}</td></tr>
|
||
<tr><td>{{th "install.trouble.check.see"}}</td><td>{{th "install.trouble.check.do"}}</td></tr>
|
||
<tr><td>{{th "install.trouble.node.see"}}</td><td>{{th "install.trouble.node.do"}}</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
<span>llmgw {{.Version}} · {{t "install.footer.recorded" .Recorded}}</span>
|
||
<span><a href="{{.AboutURL}}">{{t "install.nav.about"}}</a> ·
|
||
<a href="https://gitea.mynodes.xyz/ilker/llmgw/src/branch/main/docs/install.md">{{t "install.footer.full"}}</a></span>{{if not .Site}}{{template "langs"}}{{end}}
|
||
</footer>
|
||
</div>
|
||
|
||
<script>
|
||
var say = { copy: {{t "about.copy"}}, copied: {{t "about.copied"}}, selected: {{t "about.selected"}},
|
||
play: {{t "install.play"}}, pause: {{t "install.pause"}}, again: {{t "install.again"}} };
|
||
document.querySelectorAll('.copy').forEach(function (b) {
|
||
b.addEventListener('click', function () {
|
||
var text = b.parentElement.querySelector('pre').innerText;
|
||
var done = function (label) { b.textContent = label; setTimeout(function () { b.textContent = say.copy; }, 1600); };
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(function () { done(say.copied); }, select);
|
||
} else { select(); }
|
||
function select() {
|
||
var r = document.createRange(); r.selectNodeContents(b.parentElement.querySelector('pre'));
|
||
var s = window.getSelection(); s.removeAllRanges(); s.addRange(r); done(say.selected);
|
||
}
|
||
});
|
||
});
|
||
|
||
// The player: a recorded session, frame by frame. The page shows its last
|
||
// screen without it; this only adds the button that plays it back.
|
||
document.querySelectorAll('.tty[data-src]').forEach(function (el) {
|
||
var screen = el.querySelector('.tty-screen');
|
||
var bar = el.querySelector('.tty-progress i'), btn = el.querySelector('.tty-play');
|
||
var cast = null, total = 0, lines = [], i = 0, elapsed = 0, timer = null;
|
||
btn.hidden = false;
|
||
function show(k) {
|
||
var f = cast.frames[k];
|
||
if (f.l) { lines = f.l.slice(); } else { for (var r in f.c) { lines[+r] = f.c[r]; } }
|
||
screen.innerHTML = lines.join('\n');
|
||
}
|
||
function tick() {
|
||
if (i >= cast.frames.length) { timer = null; btn.textContent = say.again; return; }
|
||
show(i); elapsed += cast.frames[i].d; bar.style.width = (100 * elapsed / total) + '%';
|
||
timer = setTimeout(tick, cast.frames[i].d * 1000); i++;
|
||
}
|
||
function start() {
|
||
if (i >= cast.frames.length) { i = 0; elapsed = 0; lines = []; }
|
||
btn.textContent = say.pause; tick();
|
||
}
|
||
btn.addEventListener('click', function () {
|
||
if (timer) { clearTimeout(timer); timer = null; btn.textContent = say.play; return; }
|
||
if (cast) { start(); return; }
|
||
// Fetched only now: most readers never play most recordings.
|
||
btn.disabled = true;
|
||
fetch(el.dataset.src).then(function (r) { return r.json(); }).then(function (c) {
|
||
cast = c; total = c.frames.reduce(function (s, f) { return s + f.d; }, 0);
|
||
// The whole terminal, so the page does not jump as the session fills it.
|
||
screen.style.minHeight = 'calc(' + (c.rows * 1.22) + 'em + 24px)';
|
||
btn.disabled = false; start();
|
||
}, function () { btn.disabled = false; });
|
||
});
|
||
});
|
||
</script>
|
||
{{if not .Site}}</body></html>{{end}}{{end}}
|
||
|
||
{{define "tty"}}<figure class="tty" data-src="{{.Src}}">
|
||
<div class="tty-bar"><span class="tty-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||
<span class="tty-title" translate="no">ilker@llm-1 · {{.Cols}}×{{.Rows}}</span>
|
||
<button class="tty-play" type="button" hidden>{{t "install.play"}}</button></div>
|
||
<pre class="tty-screen" translate="no" aria-label="{{t "install.tty_label"}}">{{.Poster}}</pre>
|
||
<div class="tty-progress"><i></i></div>
|
||
<figcaption>{{t (print "install.cast." .Name)}}</figcaption>
|
||
</figure>{{end}}
|
||
|
||
{{define "still"}}<figure class="tty">
|
||
<div class="tty-bar"><span class="tty-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||
<span class="tty-title" translate="no">sudo llmgw · {{.Label}}</span></div>
|
||
<pre class="tty-screen" translate="no" aria-label="{{t "install.tty_label"}}">{{.Screen}}</pre>
|
||
</figure>{{end}}
|