@oharness/server 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/i18n.js +354 -62
  2. package/dist/i18n.js.map +1 -1
  3. package/dist/main.js +4 -0
  4. package/dist/main.js.map +1 -1
  5. package/dist/page/client/account.js +430 -23
  6. package/dist/page/client/account.js.map +1 -1
  7. package/dist/page/client/base.js +20 -1
  8. package/dist/page/client/base.js.map +1 -1
  9. package/dist/page/client/composer.js +693 -54
  10. package/dist/page/client/composer.js.map +1 -1
  11. package/dist/page/client/computer.d.ts +27 -0
  12. package/dist/page/client/computer.js +358 -0
  13. package/dist/page/client/computer.js.map +1 -0
  14. package/dist/page/client/dialogs.js +93 -6
  15. package/dist/page/client/dialogs.js.map +1 -1
  16. package/dist/page/client/events.js +94 -7
  17. package/dist/page/client/events.js.map +1 -1
  18. package/dist/page/client/files.js +86 -1
  19. package/dist/page/client/files.js.map +1 -1
  20. package/dist/page/client/index.js +4 -0
  21. package/dist/page/client/index.js.map +1 -1
  22. package/dist/page/client/panels.js +10 -6
  23. package/dist/page/client/panels.js.map +1 -1
  24. package/dist/page/client/routines.js +296 -41
  25. package/dist/page/client/routines.js.map +1 -1
  26. package/dist/page/client/shell.js +187 -51
  27. package/dist/page/client/shell.js.map +1 -1
  28. package/dist/page/client/state.js +273 -19
  29. package/dist/page/client/state.js.map +1 -1
  30. package/dist/page/client/tasks.d.ts +8 -0
  31. package/dist/page/client/tasks.js +293 -0
  32. package/dist/page/client/tasks.js.map +1 -0
  33. package/dist/page/client/transcript.js +137 -5
  34. package/dist/page/client/transcript.js.map +1 -1
  35. package/dist/page/client/usage.js +1 -11
  36. package/dist/page/client/usage.js.map +1 -1
  37. package/dist/page/style.js +568 -60
  38. package/dist/page/style.js.map +1 -1
  39. package/dist/page.js +90 -11
  40. package/dist/page.js.map +1 -1
  41. package/dist/prefs.d.ts +14 -0
  42. package/dist/prefs.js +8 -1
  43. package/dist/prefs.js.map +1 -1
  44. package/dist/remote-crypto.d.ts +78 -0
  45. package/dist/remote-crypto.js +216 -0
  46. package/dist/remote-crypto.js.map +1 -0
  47. package/dist/remote.d.ts +103 -0
  48. package/dist/remote.js +498 -0
  49. package/dist/remote.js.map +1 -0
  50. package/dist/server.d.ts +70 -0
  51. package/dist/server.js +1291 -49
  52. package/dist/server.js.map +1 -1
  53. package/dist/starters.d.ts +92 -0
  54. package/dist/starters.js +213 -0
  55. package/dist/starters.js.map +1 -0
  56. package/dist/turn-shots.d.ts +27 -0
  57. package/dist/turn-shots.js +71 -0
  58. package/dist/turn-shots.js.map +1 -0
  59. package/package.json +2 -2
@@ -462,7 +462,11 @@ function openConnect(providerId, thenModel) {
462
462
  // the providers that are one. For a vendor whose credential is a key it
463
463
  // would be an advertisement in the way of the field they came to fill in.
464
464
  document.getElementById("cn-aside").hidden = !provider.signIn;
465
- document.getElementById("cn-title").textContent = provider.label || provider.id;
465
+ // The column beside it already names OHarness; the title says what this
466
+ // side is for instead of naming it a second time.
467
+ document.getElementById("cn-title").textContent = provider.signIn && provider.readiness !== "no-account-system"
468
+ ? t("models.signIn")
469
+ : provider.label || provider.id;
466
470
  // What this dialog is answering. A credential that ran out and one that was
467
471
  // never created both arrive here, and telling someone to "sign in" when what
468
472
  // happened is "your session ended" leaves them wondering whether the first
@@ -1016,6 +1020,7 @@ async function loadAccount() {
1016
1020
  return found ? found.name : t("account.teamUnnamed");
1017
1021
  };
1018
1022
  void getJson("/billing").then((state) => {
1023
+ if (state && !state.error) lastBilling = state;
1019
1024
  if (!state || !state.signedIn || state.unavailable) return;
1020
1025
  sellsCredits = true;
1021
1026
  // The plan, from the same answer the balance comes out of: "0 credits"
@@ -1156,8 +1161,31 @@ async function loadAccount() {
1156
1161
  // Whose credits the next call spends: Personal, or a team the account is
1157
1162
  // on, the current one ticked the way the model menu ticks its choice.
1158
1163
  // Only once there is a team to choose, since a list of one is no choice.
1164
+ // The team page on the site, in the real browser, where a team is started,
1165
+ // its people invited and its seats bought.
1166
+ const openTeams = () => {
1167
+ closeRowMenu();
1168
+ window.open(body.site + "/" + LOCALE + "/dashboard/team", "_blank", "noopener");
1169
+ };
1159
1170
  if (knownTeams.length) {
1160
- items.push(el("div", "menu-head menu-section", t("account.workspace")));
1171
+ // Managing them sits on the section's own heading, at its right, rather
1172
+ // than as one more row that reads like a workspace to switch to.
1173
+ // The heading and the choices under it on one faint surface; the one
1174
+ // in use is marked by a stronger fill (see .menu-choices).
1175
+ const group = el("div", "menu-choices");
1176
+ const head = el("div", "menu-head menu-section menu-section-row");
1177
+ head.appendChild(el("span", null, t("account.workspace")));
1178
+ if (body.site) {
1179
+ const manage = el("button", "menu-section-action", t("account.manage"));
1180
+ manage.type = "button";
1181
+ manage.title = t("account.manageTeams");
1182
+ manage.addEventListener("click", (e) => {
1183
+ e.stopPropagation();
1184
+ openTeams();
1185
+ });
1186
+ head.appendChild(manage);
1187
+ }
1188
+ group.appendChild(head);
1161
1189
  const choice = (label, id) => {
1162
1190
  const chosen = (body.team || null) === id;
1163
1191
  const item = el("button", "menu-item" + (chosen ? " on" : ""));
@@ -1167,22 +1195,14 @@ async function loadAccount() {
1167
1195
  item.addEventListener("click", () => (chosen ? closeRowMenu() : billTo(id)));
1168
1196
  return item;
1169
1197
  };
1170
- items.push(choice(t("account.personal"), null));
1171
- knownTeams.forEach((team) => items.push(choice(team.name, team.id)));
1198
+ group.appendChild(choice(t("account.personal"), null));
1199
+ knownTeams.forEach((team) => group.appendChild(choice(team.name, team.id)));
1200
+ items.push(group);
1172
1201
  }
1173
- // The way to a workspace at all, for everyone signed in: Create a team for an
1174
- // account on none, Manage workspaces once there is one. Both are the team page
1175
- // on the site, in the real browser, where a team is started, its people
1176
- // invited and its seats bought.
1177
- if (body.site) {
1178
- items.push(
1179
- menuItem(t(knownTeams.length ? "account.manageTeams" : "account.createTeam"), {
1180
- run: () => {
1181
- closeRowMenu();
1182
- window.open(body.site + "/" + LOCALE + "/dashboard/team", "_blank", "noopener");
1183
- },
1184
- }),
1185
- );
1202
+ // The way to a workspace at all, for an account on none. Once there is one,
1203
+ // the section heading above carries Manage instead.
1204
+ if (body.site && !knownTeams.length) {
1205
+ items.push(menuItem(t("account.createTeam"), { run: openTeams }));
1186
1206
  }
1187
1207
  // The two pages this menu had no way to reach, under the workspace rows
1188
1208
  // they belong beside: what a team costs and how to ask for the parts of it
@@ -1190,6 +1210,16 @@ async function loadAccount() {
1190
1210
  // in the real browser like every other outbound item here — in the desktop
1191
1211
  // build the window handler routes them there rather than loading the site
1192
1212
  // inside the window the session lives in.
1213
+ // Feedback goes to the site as this account, so it lives in the menu that
1214
+ // only exists while one is signed in.
1215
+ items.push(
1216
+ menuItem(t("feedback.menu"), {
1217
+ run: () => {
1218
+ closeRowMenu();
1219
+ openFeedback();
1220
+ },
1221
+ }),
1222
+ );
1193
1223
  if (body.site) {
1194
1224
  items.push(
1195
1225
  menuItem(t("account.enterprise"), {
@@ -1420,6 +1450,67 @@ function applyTheme() {
1420
1450
  applyTheme();
1421
1451
 
1422
1452
  let pwInterval = "month";
1453
+ /**
1454
+ * The last billing answer this window saw — the account menu asks for it when
1455
+ * the account loads, and the paywall on every poll. The paywall opens on it
1456
+ * rather than on "Checking the balance…": the plans and their prices do not
1457
+ * change between one minute and the next, and a dialog that makes someone
1458
+ * wait for a price list it already had reads as slow. The fresh answer
1459
+ * replaces it the moment it lands.
1460
+ */
1461
+ let lastBilling = null;
1462
+ // Set by a click on the Yearly tab and spent by the render that follows it:
1463
+ // the yearly prices count down from twelve months at the monthly price once,
1464
+ // when the reader asks for the year. Not on every repaint — the balance poll
1465
+ // redraws this body every few seconds, and a number that kept counting would
1466
+ // read as a price that kept changing.
1467
+ let pwCount = false;
1468
+
1469
+ /**
1470
+ * The models a plan's credits reach, drawn as marks and names above the plans:
1471
+ * someone stopped mid-conversation by this dialog is deciding whether the
1472
+ * money buys the models they care about, and a name with its mark answers
1473
+ * that faster than a sentence. Named here rather than fetched: this screen has
1474
+ * only the billing catalogue, which knows plans and not models, and these are
1475
+ * the ones the site's pricing page leads with. The marks are inline for the
1476
+ * reason the model menu draws ours inline — no request to anyone to open a
1477
+ * paywall.
1478
+ */
1479
+ const PW_MODELS = [
1480
+ {
1481
+ key: "paywall.chat",
1482
+ models: [
1483
+ { label: "GPT-6 Astra", svg: '<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"/></svg>' },
1484
+ { label: "Opus 5.5", svg: '<svg viewBox="0 0 24 24"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"/></svg>' },
1485
+ { label: "DeepSeek V4.1", svg: '<svg viewBox="0 0 24 24"><path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z" fill="#4D6BFE"/></svg>' },
1486
+ { label: "Kimi K3", svg: '<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"/><path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"/></svg>' },
1487
+ ],
1488
+ },
1489
+ {
1490
+ key: "paywall.media",
1491
+ models: [
1492
+ { label: "GPT Image 2.5", svg: '<svg fill="currentColor" fill-rule="evenodd" viewBox="0 0 24 24"><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"/></svg>' },
1493
+ { label: "Seedance 2.5", svg: '<svg viewBox="0 0 24 24"><path d="M14.944 18.587l-1.704-.445V10.01l1.824-.462c1-.254 1.84-.461 1.88-.453.032 0 .056 2.235.056 4.972v4.973l-.176-.008c-.104 0-.952-.207-1.88-.446z" fill="#00C8D2" fill-rule="nonzero"/><path d="M7 16.542c0-2.736.024-4.98.064-4.98.032-.008.872.2 1.88.454l1.816.461-.016 4.05-.024 4.049-1.632.422c-.896.23-1.736.445-1.856.469L7 21.523v-4.98z" fill="#3C8CFF" fill-rule="nonzero"/><path d="M19.24 12.477c0-9.03.008-9.515.144-9.475.072.024.784.207 1.576.406.792.207 1.576.405 1.744.445l.296.08-.016 8.56-.024 8.568-1.624.414c-.888.23-1.728.437-1.856.47l-.24.055v-9.523z" fill="#78E6DC" fill-rule="nonzero"/><path d="M1 12.509c0-4.678.024-8.505.064-8.505.032 0 .872.207 1.872.454l1.824.461v7.582c0 4.16-.016 7.574-.032 7.574-.024 0-.872.215-1.88.47L1 21.013v-8.505z" fill="#325AB4"/></svg>' },
1494
+ ],
1495
+ },
1496
+ ];
1497
+
1498
+ /** A price that counts down to its own text, once. Reduced motion skips it. */
1499
+ function countDown(node, fromCents, toCents, currency, text) {
1500
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1501
+ const start = performance.now();
1502
+ const tick = (now) => {
1503
+ const p = Math.min(1, (now - start) / 700);
1504
+ const eased = 1 - Math.pow(1 - p, 3);
1505
+ // Whole units on the way down, so the cents do not flicker; the last
1506
+ // frame is the real string.
1507
+ const cents = Math.round((fromCents + (toCents - fromCents) * eased) / 100) * 100;
1508
+ if (!node.isConnected) return;
1509
+ node.textContent = p < 1 ? moneyOf(cents, currency) : text;
1510
+ if (p < 1) requestAnimationFrame(tick);
1511
+ };
1512
+ requestAnimationFrame(tick);
1513
+ }
1423
1514
 
1424
1515
  /**
1425
1516
  * @param buying Opened deliberately, rather than by a turn that could not be
@@ -1594,13 +1685,18 @@ function openPaywall(buying) {
1594
1685
  * months at the same price, and a "Save 0%" badge on it would be a worse
1595
1686
  * lie than silence.
1596
1687
  */
1597
- const savingOn = (plan) => {
1688
+ /** Twelve months of the same tier at the monthly price, where the year costs less. */
1689
+ const fullYearOf = (plan) => {
1598
1690
  if (plan.interval !== "year" || !plan.tier) return 0;
1599
1691
  const monthly = subs.find((p) => p.tier === plan.tier && p.interval === "month");
1600
1692
  if (!monthly || typeof monthly.amountCents !== "number" || typeof plan.amountCents !== "number") return 0;
1693
+ if ((monthly.currency || "usd").toLowerCase() !== (plan.currency || "usd").toLowerCase()) return 0;
1601
1694
  const full = monthly.amountCents * 12;
1602
- if (full <= 0 || plan.amountCents >= full) return 0;
1603
- return Math.round((1 - plan.amountCents / full) * 100);
1695
+ return full > 0 && plan.amountCents < full ? full : 0;
1696
+ };
1697
+ const savingOn = (plan) => {
1698
+ const full = fullYearOf(plan);
1699
+ return full ? Math.round((1 - plan.amountCents / full) * 100) : 0;
1604
1700
  };
1605
1701
 
1606
1702
  /** One row: what it is, how many credits, and how much money. */
@@ -1618,7 +1714,19 @@ function openPaywall(buying) {
1618
1714
  if (plan.popular) name.appendChild(el("span", "pw-tag", t("paywall.popular")));
1619
1715
  top.appendChild(name);
1620
1716
  const money = moneyOf(plan.amountCents, plan.currency);
1621
- if (money) top.appendChild(el("span", "pw-plan-price", money + (suffix || "")));
1717
+ if (money) {
1718
+ const price = el("span", "pw-plan-price");
1719
+ // A yearly rung that saves shows what it saves from: twelve months at
1720
+ // the monthly price, struck through, and — right after the switch to
1721
+ // the year — the price counting down from that number to its own.
1722
+ const full = fullYearOf(plan);
1723
+ if (full) price.appendChild(el("s", "pw-was", moneyOf(full, plan.currency)));
1724
+ const amountNode = el("span", null, money);
1725
+ price.appendChild(amountNode);
1726
+ if (suffix) price.appendChild(document.createTextNode(suffix));
1727
+ if (full && pwCount) countDown(amountNode, full, plan.amountCents, plan.currency, money);
1728
+ top.appendChild(price);
1729
+ }
1622
1730
  row.appendChild(top);
1623
1731
  // The pack's own label is already "2,000 credits", so repeating the
1624
1732
  // number underneath it said the same thing twice. Subscriptions need
@@ -1656,14 +1764,42 @@ function openPaywall(buying) {
1656
1764
  const other = subs.filter((p) => p.interval !== "month" && p.interval !== "year");
1657
1765
 
1658
1766
  if (subs.length) {
1767
+ // What the credits reach, before the plans that sell them: someone who
1768
+ // came here to keep chatting may not know the same balance runs the
1769
+ // other flagship models, and draws pictures and video too.
1770
+ const media = el("div", "pw-media");
1771
+ media.appendChild(el("div", "pw-media-head", t("paywall.included")));
1772
+ PW_MODELS.forEach((group) => {
1773
+ media.appendChild(el("span", "pw-media-kind", t(group.key)));
1774
+ const marks = el("span", "pw-media-models");
1775
+ group.models.forEach((model) => {
1776
+ const chip = el("span", "pw-media-model");
1777
+ const mark = el("span", "pw-media-mark");
1778
+ mark.innerHTML = model.svg;
1779
+ chip.appendChild(mark);
1780
+ chip.appendChild(document.createTextNode(model.label));
1781
+ marks.appendChild(chip);
1782
+ });
1783
+ media.appendChild(marks);
1784
+ });
1785
+ body.appendChild(media);
1786
+
1659
1787
  if (intervals.length > 1) {
1660
1788
  const toggle = el("div", "pw-toggle");
1789
+ // The best a year saves, on the tab itself: the reason to press it,
1790
+ // said before it is pressed.
1791
+ const best = Math.max(0, ...byInterval.year.map(savingOn));
1661
1792
  intervals.forEach((key) => {
1662
1793
  const tab = el("button", "pw-tab" + (key === pwInterval ? " on" : ""));
1663
1794
  tab.type = "button";
1664
1795
  tab.textContent = t(key === "year" ? "paywall.yearly" : "paywall.monthly");
1796
+ if (key === "year" && best > 0) {
1797
+ tab.appendChild(el("span", "pw-tab-save", t("paywall.upTo", { percent: best })));
1798
+ }
1665
1799
  tab.onclick = () => {
1800
+ if (pwInterval === key) return;
1666
1801
  pwInterval = key;
1802
+ pwCount = key === "year";
1667
1803
  render(state);
1668
1804
  };
1669
1805
  toggle.appendChild(tab);
@@ -1675,6 +1811,7 @@ function openPaywall(buying) {
1675
1811
  showing.concat(other).forEach((plan) => {
1676
1812
  list.appendChild(planRow(plan, t(plan.interval === "year" ? "paywall.perYear" : "paywall.perMonth")));
1677
1813
  });
1814
+ pwCount = false;
1678
1815
  body.appendChild(list);
1679
1816
  }
1680
1817
 
@@ -1725,6 +1862,7 @@ function openPaywall(buying) {
1725
1862
 
1726
1863
  const check = () =>
1727
1864
  getJson("/billing").then((state) => {
1865
+ if (state && !state.error) lastBilling = state;
1728
1866
  if (!dialog.open) return;
1729
1867
  if (!state || state.error) {
1730
1868
  return void render({ signedIn: true, unavailable: "unreachable" });
@@ -1743,8 +1881,21 @@ function openPaywall(buying) {
1743
1881
  render(state);
1744
1882
  });
1745
1883
 
1746
- body.replaceChildren(el("div", "cn-note", t("paywall.checking")));
1747
- if (!dialog.open) dialog.showModal();
1884
+ // The cards straight away from what is already known. The balance too when
1885
+ // it was already zero — the usual case, and the line then does not jump in
1886
+ // above the cards a moment later — but not a positive one: that number is
1887
+ // stale by definition, since running out is why this opened, and render
1888
+ // leaves the line out when it is undefined. A team's screen is all its
1889
+ // pooled number, so that one waits for the fresh answer.
1890
+ const cached = lastBilling;
1891
+ if (cached && cached.signedIn && !cached.unavailable && !cached.team
1892
+ && Array.isArray(cached.plans) && cached.plans.length) {
1893
+ if (!dialog.open) dialog.showModal();
1894
+ render({ ...cached, balanceMicro: Number(cached.balanceMicro) === 0 ? 0 : undefined });
1895
+ } else {
1896
+ body.replaceChildren(el("div", "cn-note", t("paywall.checking")));
1897
+ if (!dialog.open) dialog.showModal();
1898
+ }
1748
1899
  void check();
1749
1900
  if (paywallTimer) clearInterval(paywallTimer);
1750
1901
  paywallTimer = setInterval(() => void check(), PAYWALL_POLL_MS);
@@ -1891,5 +2042,261 @@ document.getElementById("nav-models").addEventListener("click", () => {
1891
2042
  panelBody.replaceChildren(panelLoading());
1892
2043
  void showModels();
1893
2044
  });
2045
+
2046
+ /**
2047
+ * The feedback dialog: a kind, a message, screenshots or recordings, and Send.
2048
+ *
2049
+ * Sent through this app's server, which holds the account's token and adds
2050
+ * the platform; the page adds the app version, which only the desktop shell
2051
+ * knows. Files are picked, pasted or dropped, and uploaded one at a time when
2052
+ * Send is pressed — not when attached, so a file removed again was never sent
2053
+ * anywhere. The result is said inside the dialog rather than as a notice in the
2054
+ * transcript, which is not where anybody sending feedback is looking.
2055
+ */
2056
+ const FEEDBACK_KINDS = ["bug", "idea", "other"];
2057
+ const FEEDBACK_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp", "video/mp4", "video/webm", "video/quicktime"];
2058
+ const FEEDBACK_MAX_FILES = 5;
2059
+ const FEEDBACK_MAX_BYTES = 50 * 1024 * 1024;
2060
+ let feedbackKind = "bug";
2061
+ /** What is attached: the file, a preview URL, and once uploaded, what the server answered. */
2062
+ let feedbackFiles = [];
2063
+ let feedbackWired = false;
2064
+
2065
+ function feedbackSize(bytes) {
2066
+ return bytes >= 1048576 ? (bytes / 1048576).toFixed(1) + " MB" : Math.max(1, Math.round(bytes / 1024)) + " KB";
2067
+ }
2068
+
2069
+ function feedbackSay(text, error) {
2070
+ const status = document.getElementById("fb-status");
2071
+ status.textContent = text;
2072
+ status.style.color = error ? "var(--err)" : "";
2073
+ }
2074
+
2075
+ /** Take files from the picker, a paste or a drop, saying why any are refused. */
2076
+ function feedbackAdd(files) {
2077
+ for (const file of files) {
2078
+ if (feedbackFiles.length >= FEEDBACK_MAX_FILES) {
2079
+ feedbackSay(t("feedback.tooMany"), true);
2080
+ break;
2081
+ }
2082
+ if (!FEEDBACK_TYPES.includes(file.type)) {
2083
+ feedbackSay(t("feedback.badType", { name: file.name || "file" }), true);
2084
+ continue;
2085
+ }
2086
+ if (file.size > FEEDBACK_MAX_BYTES) {
2087
+ feedbackSay(t("feedback.tooBig", { name: file.name || "file" }), true);
2088
+ continue;
2089
+ }
2090
+ // A pasted screenshot arrives as "image.png" every time; give it a name
2091
+ // that tells two of them apart on the internal page.
2092
+ const name = file.name && file.name !== "image.png" ? file.name : "screenshot-" + (feedbackFiles.length + 1) + ".png";
2093
+ feedbackFiles.push({ file, name, preview: URL.createObjectURL(file), uploaded: null, state: null });
2094
+ }
2095
+ feedbackPaintFiles();
2096
+ }
2097
+
2098
+ function feedbackRemove(entry) {
2099
+ URL.revokeObjectURL(entry.preview);
2100
+ feedbackFiles = feedbackFiles.filter((other) => other !== entry);
2101
+ feedbackPaintFiles();
2102
+ }
2103
+
2104
+ function feedbackClearFiles() {
2105
+ for (const entry of feedbackFiles) URL.revokeObjectURL(entry.preview);
2106
+ feedbackFiles = [];
2107
+ feedbackPaintFiles();
2108
+ }
2109
+
2110
+ function feedbackPaintFiles() {
2111
+ const list = document.getElementById("fb-files");
2112
+ const tiles = feedbackFiles.map((entry) => {
2113
+ const video = entry.file.type.startsWith("video/");
2114
+ const tile = el("div", "fb-tile" + (video ? " is-video" : "") + (entry.state ? " is-" + entry.state : ""));
2115
+ tile.title = entry.name + " \u00b7 " + feedbackSize(entry.file.size);
2116
+ const thumb = el(video ? "video" : "img", "fb-thumb");
2117
+ thumb.src = entry.preview;
2118
+ if (video) {
2119
+ thumb.muted = true;
2120
+ thumb.preload = "metadata";
2121
+ tile.appendChild(thumb);
2122
+ tile.appendChild(el("span", "fb-play"));
2123
+ } else {
2124
+ thumb.alt = entry.name;
2125
+ tile.appendChild(thumb);
2126
+ }
2127
+ // Only while a send is in flight: one tile uploading, the ones before it done.
2128
+ if (entry.state === "uploading") tile.appendChild(el("span", "fb-spinner"));
2129
+ if (entry.state === "done") tile.appendChild(el("span", "fb-done", "\u2713"));
2130
+ const remove = el("button", "fb-remove", "\u00d7");
2131
+ remove.type = "button";
2132
+ remove.title = t("feedback.remove");
2133
+ remove.setAttribute("aria-label", t("feedback.remove") + " " + entry.name);
2134
+ remove.disabled = Boolean(entry.state);
2135
+ remove.addEventListener("click", () => feedbackRemove(entry));
2136
+ tile.appendChild(remove);
2137
+ return tile;
2138
+ });
2139
+ if (feedbackFiles.length < FEEDBACK_MAX_FILES) {
2140
+ const add = el("button", "fb-tile fb-add");
2141
+ add.type = "button";
2142
+ add.setAttribute("aria-label", t("feedback.attach"));
2143
+ add.title = t("feedback.attach");
2144
+ add.append(svgIcon("image", "fb-add-icon"), el("span", "fb-add-label", t("feedback.add")));
2145
+ add.addEventListener("click", () => document.getElementById("fb-file").click());
2146
+ tiles.push(add);
2147
+ }
2148
+ // What can go here and how much, beside the tiles rather than on a line of
2149
+ // its own: before anything is attached it says what to attach and how;
2150
+ // after, how many of the five are used.
2151
+ const caption = el("div", "fb-caption");
2152
+ caption.appendChild(el("span", "fb-caption-title", t("feedback.mediaTitle")));
2153
+ caption.appendChild(
2154
+ el(
2155
+ "span",
2156
+ "fb-caption-sub",
2157
+ feedbackFiles.length
2158
+ ? t("feedback.mediaCount", { n: feedbackFiles.length, max: FEEDBACK_MAX_FILES })
2159
+ : t("feedback.mediaHint"),
2160
+ ),
2161
+ );
2162
+ tiles.push(caption);
2163
+ list.replaceChildren(...tiles);
2164
+ }
2165
+
2166
+ /** Upload one file through this server; the answer is what the row stores. */
2167
+ async function feedbackUpload(entry) {
2168
+ if (entry.uploaded) return entry.uploaded;
2169
+ const response = await fetch(q("/feedback/upload"), {
2170
+ method: "POST",
2171
+ headers: { "content-type": entry.file.type, "x-filename": encodeURIComponent(entry.name) },
2172
+ body: entry.file,
2173
+ signal: AbortSignal.timeout(15 * 60 * 1000),
2174
+ }).catch((err) => ({ ok: false, json: async () => ({ error: String(err.message || err) }) }));
2175
+ const body = await response.json().catch(() => ({}));
2176
+ if (!response.ok || !body.path) throw new Error(body.error || "upload failed");
2177
+ // Kept, so a retry after a later file fails does not send this one twice.
2178
+ entry.uploaded = { path: body.path, name: body.name || entry.name, type: body.type || entry.file.type, size: body.size || entry.file.size };
2179
+ return entry.uploaded;
2180
+ }
2181
+
2182
+ /** The listeners that outlive one opening: the picker, paste and drop. */
2183
+ function wireFeedback(dialog) {
2184
+ if (feedbackWired) return;
2185
+ feedbackWired = true;
2186
+ const picker = document.getElementById("fb-file");
2187
+ picker.addEventListener("change", () => {
2188
+ feedbackAdd([...picker.files]);
2189
+ picker.value = "";
2190
+ });
2191
+ // A screenshot on the clipboard is the most common attachment of all.
2192
+ document.getElementById("fb-message").addEventListener("paste", (e) => {
2193
+ const files = [...(e.clipboardData ? e.clipboardData.files : [])];
2194
+ if (!files.length) return;
2195
+ e.preventDefault();
2196
+ feedbackAdd(files);
2197
+ });
2198
+ dialog.addEventListener("dragover", (e) => {
2199
+ if (!e.dataTransfer || ![...e.dataTransfer.types].includes("Files")) return;
2200
+ e.preventDefault();
2201
+ dialog.classList.add("fb-dropping");
2202
+ });
2203
+ dialog.addEventListener("dragleave", (e) => {
2204
+ if (e.target === dialog) dialog.classList.remove("fb-dropping");
2205
+ });
2206
+ dialog.addEventListener("drop", (e) => {
2207
+ dialog.classList.remove("fb-dropping");
2208
+ if (!e.dataTransfer || !e.dataTransfer.files.length) return;
2209
+ e.preventDefault();
2210
+ feedbackAdd([...e.dataTransfer.files]);
2211
+ });
2212
+ }
2213
+
2214
+ function openFeedback() {
2215
+ const dialog = document.getElementById("feedback-dialog");
2216
+ const kinds = document.getElementById("fb-kinds");
2217
+ const message = document.getElementById("fb-message");
2218
+ const send = document.getElementById("fb-send");
2219
+ const cancel = document.getElementById("fb-cancel");
2220
+ wireFeedback(dialog);
2221
+
2222
+ const paintKinds = () => {
2223
+ kinds.replaceChildren(
2224
+ ...FEEDBACK_KINDS.map((kind) => {
2225
+ const chip = el("button", "fb-kind" + (kind === feedbackKind ? " on" : ""), t("feedback.kind." + kind));
2226
+ chip.type = "button";
2227
+ chip.setAttribute("role", "radio");
2228
+ chip.setAttribute("aria-checked", String(kind === feedbackKind));
2229
+ chip.addEventListener("click", () => {
2230
+ feedbackKind = kind;
2231
+ paintKinds();
2232
+ });
2233
+ return chip;
2234
+ }),
2235
+ );
2236
+ };
2237
+
2238
+ paintKinds();
2239
+ feedbackPaintFiles();
2240
+ message.placeholder = t("feedback.placeholder");
2241
+ feedbackSay("");
2242
+ send.disabled = false;
2243
+ send.textContent = t("feedback.send");
2244
+ cancel.onclick = () => dialog.close();
2245
+ // A draft — text and files — survives closing the dialog by accident; it is
2246
+ // cleared only once sent.
2247
+ send.onclick = async () => {
2248
+ const text = message.value.trim();
2249
+ if (!text) {
2250
+ feedbackSay(t("feedback.empty"), true);
2251
+ return void message.focus();
2252
+ }
2253
+ send.disabled = true;
2254
+ const attachments = [];
2255
+ try {
2256
+ for (let i = 0; i < feedbackFiles.length; i++) {
2257
+ const entry = feedbackFiles[i];
2258
+ send.textContent = t("feedback.uploading", { n: i + 1, total: feedbackFiles.length });
2259
+ entry.state = entry.uploaded ? "done" : "uploading";
2260
+ feedbackPaintFiles();
2261
+ attachments.push(await feedbackUpload(entry));
2262
+ entry.state = "done";
2263
+ feedbackPaintFiles();
2264
+ }
2265
+ } catch (err) {
2266
+ // Back to editable; files already uploaded keep their answer, so a
2267
+ // retry only sends the rest.
2268
+ for (const entry of feedbackFiles) entry.state = null;
2269
+ feedbackPaintFiles();
2270
+ send.disabled = false;
2271
+ send.textContent = t("feedback.send");
2272
+ return void feedbackSay(String(err.message || err), true);
2273
+ }
2274
+ send.textContent = t("feedback.sending");
2275
+ const bridge = window.oharness;
2276
+ const result = await api("/feedback", {
2277
+ kind: feedbackKind,
2278
+ message: text,
2279
+ source: bridge && bridge.desktop ? "desktop" : "web",
2280
+ appVersion: (bridge && bridge.app && bridge.app.version) || undefined,
2281
+ attachments,
2282
+ });
2283
+ send.textContent = t("feedback.send");
2284
+ if (!result || result.error) {
2285
+ for (const entry of feedbackFiles) entry.state = null;
2286
+ feedbackPaintFiles();
2287
+ send.disabled = false;
2288
+ return void feedbackSay((result && result.error) || "could not send", true);
2289
+ }
2290
+ track("feedback_sent", { kind: feedbackKind, attachments: attachments.length });
2291
+ message.value = "";
2292
+ feedbackClearFiles();
2293
+ feedbackSay(t("feedback.sent"));
2294
+ setTimeout(() => {
2295
+ if (dialog.open) dialog.close();
2296
+ }, 1400);
2297
+ };
2298
+ if (!dialog.open) dialog.showModal();
2299
+ message.focus();
2300
+ }
1894
2301
  `;
1895
2302
  //# sourceMappingURL=account.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"account.js","sourceRoot":"","sources":["../../../src/page/client/account.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA81DhC,CAAC"}
1
+ {"version":3,"file":"account.js","sourceRoot":"","sources":["../../../src/page/client/account.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqvEhC,CAAC"}
@@ -68,6 +68,8 @@ const ICON_PATHS = {
68
68
  // The audio card's button. Drawn in outline like the rest and filled by the
69
69
  // card's CSS, so the triangle keeps the set's rounded joins.
70
70
  play: ["M5.5 3.9v8.2a.6.6 0 0 0 .92.5l6.3-4.1a.6.6 0 0 0 0-1L6.42 3.4a.6.6 0 0 0-.92.5z"],
71
+ // The composer's send arrow, for the boxes that send somewhere else.
72
+ send: ["M8 13V3.4M3.6 7.8 8 3.2l4.4 4.6"],
71
73
  pause: ["M5.25 3.75v8.5", "M10.75 3.75v8.5"],
72
74
  // A padlock: the shackle as a path, the body as a rect.
73
75
  lock: ["M5.33 7.33V5.33a2.67 2.67 0 0 1 5.34 0v2"],
@@ -89,6 +91,10 @@ const ICON_PATHS = {
89
91
  "M3.33 3h2.87a1.33 1.33 0 0 1 1.09.57l.85 1.22a1.33 1.33 0 0 0 1.09.57H12.67" +
90
92
  "A1.33 1.33 0 0 1 14 6.7v4.97A1.33 1.33 0 0 1 12.67 13H3.33" +
91
93
  "A1.33 1.33 0 0 1 2 11.67V4.33A1.33 1.33 0 0 1 3.33 3Z",
94
+ // The back panel's edge, carried across the front: what makes this a
95
+ // closed folder rather than a tabbed card, and the line the open one
96
+ // tips forward from.
97
+ "M2 7h12",
92
98
  ],
93
99
  folderOpen: [
94
100
  "M4.4 10.3l.87-1.71a1.33 1.33 0 0 1 1.19-.72H12.8a1.33 1.33 0 0 1 1.2 1.6" +
@@ -138,6 +144,11 @@ const ICON_PATHS = {
138
144
  close: ["M4.5 4.5l7 7", "M11.5 4.5l-7 7"],
139
145
  /* Its counterpart, for adding one. */
140
146
  plus: ["M8 3.5v9", "M3.5 8h9"],
147
+ /* A picture: a frame, a hill and a sun — what "attach an image" looks like
148
+ everywhere else. */
149
+ image: ["M2.5 11.5 6 8l3 3 1.5-1.5 3 3"],
150
+ imageRects: [[2, 2.5, 12, 11, 2]],
151
+ imageCircles: [[10.5, 6, 1.2]],
141
152
  search: ["M11.5 11.5 14 14"],
142
153
  searchCircles: [[7.3, 7.3, 4.6]],
143
154
  // A shopfront: an awning over a counter, for the list of what is on offer.
@@ -150,6 +161,8 @@ const ICON_PATHS = {
150
161
  // transcript: the arc lands on the new node rather than crossing it.
151
162
  fork: ["M5 2.6v7.7", "M11 6a5.7 5.7 0 0 1-4.3 6"],
152
163
  forkCircles: [[11, 4.3], [5, 12]],
164
+ // A trunk and the branch that leaves it for a tree of its own.
165
+ worktree: ["M4.5 2v12", "M4.5 10.5c0-3.5 7-2.5 7-6", "M11.5 2.5a1 1 0 1 1 0 2 1 1 0 1 1 0-2"],
153
166
  copy: [
154
167
  "M6.67 5.33h6.66a1.33 1.33 0 0 1 1.34 1.34v6.66a1.33 1.33 0 0 1-1.34 1.34" +
155
168
  "H6.67a1.33 1.33 0 0 1-1.34-1.34V6.67a1.33 1.33 0 0 1 1.34-1.34Z",
@@ -202,6 +215,12 @@ const ICON_PATHS = {
202
215
  // picker draws, because it is the same control.
203
216
  calendar: ["M2 6.5h12M5.5 1.5v3M10.5 1.5v3"],
204
217
  calendarRects: [[2, 3, 12, 11, 2.5]],
218
+ // Goal mode: a target, rings closing on the point being aimed at.
219
+ goal: ["M8 8h.01"],
220
+ goalCircles: [[8, 8, 6], [8, 8, 3]],
221
+ // Computer use: a screen on its stand, with the pointer that drives it.
222
+ computer: ["M5.5 14h5M8 11.5V14", "M7.2 4.6l3.6 1.5-1.6.5-.5 1.6Z"],
223
+ computerRects: [[1.5, 2, 13, 9.5, 1.6]],
205
224
  chevronLeft: ["M10 3.5 5.5 8l4.5 4.5"],
206
225
  chevronRight: ["M6 3.5 10.5 8 6 12.5"],
207
226
  };
@@ -391,7 +410,7 @@ const jumpBtn = document.getElementById("jump");
391
410
  const metaEl = document.getElementById("cost");
392
411
  const tokensEl = document.getElementById("tokens");
393
412
  const cacheEl = document.getElementById("cache");
394
- const ctxBar = document.querySelector("#ctx i");
413
+ const ctxRing = document.getElementById("ctx");
395
414
  const ctxText = document.getElementById("ctxtext");
396
415
  const sessionList = document.getElementById("sessions");
397
416
  const sidebarEl = document.querySelector("body > aside");
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","sourceRoot":"","sources":["../../../src/page/client/base.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Y7B,CAAC"}
1
+ {"version":3,"file":"base.js","sourceRoot":"","sources":["../../../src/page/client/base.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8Z7B,CAAC"}