@oharness/server 0.9.2 → 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.
- package/dist/i18n.js +356 -62
- package/dist/i18n.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/page/client/account.js +430 -23
- package/dist/page/client/account.js.map +1 -1
- package/dist/page/client/base.js +20 -1
- package/dist/page/client/base.js.map +1 -1
- package/dist/page/client/composer.js +709 -58
- package/dist/page/client/composer.js.map +1 -1
- package/dist/page/client/computer.d.ts +27 -0
- package/dist/page/client/computer.js +358 -0
- package/dist/page/client/computer.js.map +1 -0
- package/dist/page/client/dialogs.js +93 -6
- package/dist/page/client/dialogs.js.map +1 -1
- package/dist/page/client/events.js +94 -7
- package/dist/page/client/events.js.map +1 -1
- package/dist/page/client/files.js +86 -1
- package/dist/page/client/files.js.map +1 -1
- package/dist/page/client/index.js +4 -0
- package/dist/page/client/index.js.map +1 -1
- package/dist/page/client/panels.js +12 -6
- package/dist/page/client/panels.js.map +1 -1
- package/dist/page/client/routines.js +296 -41
- package/dist/page/client/routines.js.map +1 -1
- package/dist/page/client/shell.js +229 -51
- package/dist/page/client/shell.js.map +1 -1
- package/dist/page/client/state.js +273 -19
- package/dist/page/client/state.js.map +1 -1
- package/dist/page/client/tasks.d.ts +8 -0
- package/dist/page/client/tasks.js +293 -0
- package/dist/page/client/tasks.js.map +1 -0
- package/dist/page/client/transcript.js +174 -6
- package/dist/page/client/transcript.js.map +1 -1
- package/dist/page/client/usage.js +1 -11
- package/dist/page/client/usage.js.map +1 -1
- package/dist/page/style.js +706 -104
- package/dist/page/style.js.map +1 -1
- package/dist/page.js +161 -54
- package/dist/page.js.map +1 -1
- package/dist/prefs.d.ts +14 -0
- package/dist/prefs.js +8 -1
- package/dist/prefs.js.map +1 -1
- package/dist/remote-crypto.d.ts +78 -0
- package/dist/remote-crypto.js +216 -0
- package/dist/remote-crypto.js.map +1 -0
- package/dist/remote.d.ts +103 -0
- package/dist/remote.js +498 -0
- package/dist/remote.js.map +1 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +1291 -49
- package/dist/server.js.map +1 -1
- package/dist/starters.d.ts +92 -0
- package/dist/starters.js +213 -0
- package/dist/starters.js.map +1 -0
- package/dist/theme.js +5 -0
- package/dist/theme.js.map +1 -1
- package/dist/turn-shots.d.ts +27 -0
- package/dist/turn-shots.js +71 -0
- package/dist/turn-shots.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1171
|
-
knownTeams.forEach((team) =>
|
|
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
|
|
1174
|
-
//
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
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)
|
|
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
|
-
|
|
1747
|
-
|
|
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
|
|
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"}
|
package/dist/page/client/base.js
CHANGED
|
@@ -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
|
|
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
|
|
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"}
|