@kevin5251984/guild 0.2.19 → 0.2.20

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.
@@ -9,7 +9,7 @@
9
9
  <link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
10
10
  <link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
11
11
  <title>Guild — 外出</title>
12
- <link rel="stylesheet" href="/mobile.css?v=away" />
12
+ <link rel="stylesheet" href="/mobile.css?v=enamel" />
13
13
  <script src="/i18n.js"></script>
14
14
  <script src="/md.js"></script>
15
15
  </head>
@@ -39,17 +39,26 @@
39
39
  <div id="mention-results"></div>
40
40
  </div>
41
41
  <textarea id="draft" rows="1" placeholder="Ask anything" data-i18n-placeholder="askAnything"></textarea>
42
+ <div class="composer-steer" id="composer-steer" hidden></div>
42
43
  <div class="composer-actions">
43
44
  <button type="submit" class="send" id="send" data-i18n="send">送出</button>
44
45
  <button type="button" class="stop-all" id="stop" hidden data-i18n="live.stop">停止</button>
46
+ <button type="button" class="steer" id="steer" hidden data-i18n="steer.insert">插入引導</button>
45
47
  </div>
46
48
  </div>
47
49
  </form>
48
50
  </section>
49
- <div class="flash" id="flash" hidden></div>
51
+ <div class="flash" id="flash" role="status" aria-live="polite" hidden></div>
50
52
  <script>
51
53
  (function () {
52
54
  const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
55
+ const SEAT_HUE = {
56
+ infra: "#5b8def",
57
+ pm: "#2ea887",
58
+ rd: "#e07a3d",
59
+ design: "#7c6af7",
60
+ marketing: "#d4537e",
61
+ };
53
62
  const state = {
54
63
  channels: [],
55
64
  bots: [],
@@ -64,6 +73,8 @@
64
73
  };
65
74
 
66
75
  function hue(key) {
76
+ const seat = SEAT_HUE[String(key).toLowerCase()];
77
+ if (seat) return seat;
67
78
  let n = 0;
68
79
  for (const ch of String(key)) n = (n + ch.charCodeAt(0) * 17) % COLORS.length;
69
80
  return COLORS[n];
@@ -80,9 +91,18 @@
80
91
  .trim()
81
92
  .split(/\s+/);
82
93
  if (parts[0] && /[A-Za-z]/.test(parts[0][0])) {
83
- return parts[0][0].toUpperCase();
94
+ const letters = parts.slice(0, 2).map((part) => part[0]).filter(Boolean);
95
+ if (
96
+ letters.length === 2 &&
97
+ /[A-Za-z]/.test(letters[0]) &&
98
+ /[A-Za-z]/.test(letters[1])
99
+ ) {
100
+ return letters.join("").toUpperCase();
101
+ }
102
+ return letters[0].toUpperCase();
84
103
  }
85
- return Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
104
+ const glyph = Array.from(String(name).replace(/\s+/g, ""))[0] || "?";
105
+ return glyph;
86
106
  }
87
107
  function portraitUrl(bot) {
88
108
  const url = bot && typeof bot.portrait === "string" ? bot.portrait.trim() : "";
@@ -121,6 +141,13 @@
121
141
  ? "/dms/" + encodeURIComponent(r.id) + "/abort"
122
142
  : "/channels/" + encodeURIComponent(r.id) + "/abort";
123
143
  }
144
+ function steerUrl() {
145
+ const r = currentRoomRef();
146
+ if (!r) return "";
147
+ return r.kind === "dm"
148
+ ? "/dms/" + encodeURIComponent(r.id) + "/steer"
149
+ : "/channels/" + encodeURIComponent(r.id) + "/steer";
150
+ }
124
151
  function parseHash() {
125
152
  const raw = location.hash.replace(/^#/, "");
126
153
  if (raw.startsWith("d/")) {
@@ -143,15 +170,17 @@
143
170
  const h = window.visualViewport ? window.visualViewport.height : window.innerHeight;
144
171
  document.documentElement.style.setProperty("--vvh", h + "px");
145
172
  }
146
- function toast(text) {
173
+ function toast(text, opts) {
147
174
  const el = document.getElementById("flash");
175
+ const ok = Boolean(opts && opts.ok);
148
176
  el.hidden = false;
149
177
  el.textContent = text;
178
+ el.classList.toggle("err", !ok);
150
179
  el.classList.add("on");
151
180
  clearTimeout(toast.timer);
152
181
  toast.timer = setTimeout(() => {
153
182
  el.classList.remove("on");
154
- }, 1800);
183
+ }, (opts && opts.ms) || 1800);
155
184
  }
156
185
  function relTime(iso) {
157
186
  if (!iso) return t("noActivity");
@@ -206,17 +235,54 @@
206
235
  .replace(/```[\s\S]*?```/g, " ")
207
236
  .replace(/`[^`]*`/g, " ");
208
237
  }
238
+ function deferredHandles(text) {
239
+ const raw = String(text || "");
240
+ const out = [];
241
+ const add = (name) => {
242
+ const key = String(name || "").toLowerCase();
243
+ if (key && out.indexOf(key) < 0) out.push(key);
244
+ };
245
+ const scan = raw.replace(/```[\s\S]*?```/g, " ");
246
+ const cue =
247
+ /(?:完成後|通過後|最後(?:才|由)?|才需要|才叫|才交)[^@\n]{0,24}@([A-Za-z0-9_-]+)/gi;
248
+ const bare =
249
+ /(?:完成後|才需要|才叫)\s*(?:call|ask|由)\s*`?@?([A-Za-z0-9_-]+)`?/gi;
250
+ let row;
251
+ while ((row = cue.exec(scan))) add(row[1]);
252
+ while ((row = bare.exec(scan))) add(row[1]);
253
+ const later =
254
+ /^[ \t]*(?:\d+[.)、]\s*|[-*•]\s+)(?:通過後|最後|然後|之後|完成後)\s*`?@([A-Za-z0-9_-]+)`?/i;
255
+ const lines = raw.split(/\r?\n/);
256
+ for (let i = 0; i < lines.length; i++) {
257
+ const hit = lines[i].match(later);
258
+ if (hit) add(hit[1]);
259
+ }
260
+ return out;
261
+ }
209
262
  function summonedBotIds(text) {
210
263
  const raw = String(text || "");
264
+ const deferred = {};
265
+ deferredHandles(raw).forEach((handle) => {
266
+ deferred[handle] = true;
267
+ });
268
+ const members = new Set(
269
+ state.kind === "channel"
270
+ ? ((currentChannel() && currentChannel().memberIds) || [])
271
+ : state.kind === "dm"
272
+ ? [state.id]
273
+ : [],
274
+ );
211
275
  const known = new Map(
212
- (state.bots || []).map((bot) => [
213
- String(bot.handle).toLowerCase(),
214
- bot.id,
215
- ]),
276
+ (state.bots || [])
277
+ .filter((bot) => !members.size || members.has(bot.id))
278
+ .map((bot) => [
279
+ String(bot.handle).toLowerCase(),
280
+ bot.id,
281
+ ]),
216
282
  );
217
283
  const out = [];
218
- const push = (id) => {
219
- if (id && out.indexOf(id) < 0) out.push(id);
284
+ const push = (id, handle) => {
285
+ if (id && !deferred[handle] && out.indexOf(id) < 0) out.push(id);
220
286
  };
221
287
  let fence = false;
222
288
  const lines = raw.split(/\r?\n/);
@@ -233,7 +299,8 @@
233
299
  if (!m) continue;
234
300
  const names = m[1].match(/@([A-Za-z0-9_-]+)/g) || [];
235
301
  for (let n = 0; n < names.length; n++) {
236
- push(known.get(names[n].slice(1).toLowerCase()));
302
+ const handle = names[n].slice(1).toLowerCase();
303
+ push(known.get(handle), handle);
237
304
  }
238
305
  }
239
306
  if (out.length) return out;
@@ -241,7 +308,8 @@
241
308
  const re = /(?:^|\s)@([A-Za-z0-9_-]+)\b/g;
242
309
  let row;
243
310
  while ((row = re.exec(scan))) {
244
- push(known.get(String(row[1]).toLowerCase()));
311
+ const handle = String(row[1]).toLowerCase();
312
+ push(known.get(handle), handle);
245
313
  }
246
314
  return out;
247
315
  }
@@ -349,6 +417,10 @@
349
417
  escapeHtml(bot.id) +
350
418
  '">' +
351
419
  t("live.stop") +
420
+ '</button><button type="button" class="turn-steer" data-live-steer="' +
421
+ escapeHtml(bot.id) +
422
+ '">' +
423
+ t("live.steer") +
352
424
  "</button></div></div></div></article>"
353
425
  );
354
426
  })
@@ -412,10 +484,28 @@
412
484
  root.innerHTML = notice + rows.map(msgHtml).join("") + turnStatusHtml();
413
485
  if (pin) root.scrollTop = root.scrollHeight;
414
486
  }
487
+ function steerModLabel() {
488
+ return /Mac|iPhone|iPad/.test(navigator.platform || "") ? "\u2318" : "Ctrl+";
489
+ }
490
+ function syncSteerHint() {
491
+ const hint = document.getElementById("composer-steer");
492
+ if (!hint) return;
493
+ if (!isBusy()) {
494
+ hint.hidden = true;
495
+ hint.textContent = "";
496
+ return;
497
+ }
498
+ const hintKey = t("steer.hint", { mod: steerModLabel() });
499
+ hint.hidden = false;
500
+ hint.textContent = hintKey === "steer.hint" ? t("live.steer") : hintKey;
501
+ }
415
502
  function syncComposer() {
416
503
  const busy = isBusy();
417
504
  document.getElementById("send").hidden = busy;
418
505
  document.getElementById("stop").hidden = !busy;
506
+ const steer = document.getElementById("steer");
507
+ if (steer) steer.hidden = !busy;
508
+ syncSteerHint();
419
509
  }
420
510
  function navRow(opts) {
421
511
  const live = opts.busy
@@ -681,6 +771,63 @@
681
771
  renderThread();
682
772
  await loadMessages().catch(() => {});
683
773
  }
774
+ async function sendSteer(raw, botId) {
775
+ const text = String(raw || "").trim();
776
+ if (!text || !state.kind) return;
777
+ const payload = { body: text };
778
+ if (botId) payload.botId = botId;
779
+ else {
780
+ const ids = botsFromSend(text);
781
+ if (ids.length === 1) payload.botId = ids[0];
782
+ }
783
+ const res = await fetch(steerUrl(), {
784
+ method: "POST",
785
+ headers: { "content-type": "application/json" },
786
+ body: JSON.stringify(payload),
787
+ });
788
+ const body = await res.json().catch(() => ({}));
789
+ if (!res.ok) throw new Error(body.error || "steer failed");
790
+ await loadMessages({ merge: true }).catch(() => {});
791
+ await pollLive();
792
+ const who = body.message && body.message.steerBotId
793
+ ? botById(body.message.steerBotId)
794
+ : null;
795
+ toast(
796
+ who ? t("steer.tagTo", { who: who.name || who.handle }) : t("steer.tag"),
797
+ { ok: true, ms: 2000 },
798
+ );
799
+ }
800
+ async function steerDraft(botId) {
801
+ const draft = document.getElementById("draft");
802
+ const raw = draft.value.trim();
803
+ if (!raw) {
804
+ draft.focus();
805
+ toast(t("live.steer"), { ok: true, ms: 1400 });
806
+ return;
807
+ }
808
+ draft.value = "";
809
+ fitDraft();
810
+ hideMention();
811
+ state.messages = state.messages.concat([
812
+ {
813
+ id: "pending-steer-" + Date.now(),
814
+ author: "you",
815
+ body: raw,
816
+ createdAt: new Date().toISOString(),
817
+ },
818
+ ]);
819
+ renderThread();
820
+ try {
821
+ await sendSteer(raw, botId);
822
+ } catch (err) {
823
+ draft.value = raw;
824
+ fitDraft();
825
+ await loadMessages().catch(() => {});
826
+ toast(err.message || String(err));
827
+ }
828
+ syncComposer();
829
+ schedule();
830
+ }
684
831
  async function sendDraft() {
685
832
  const draft = document.getElementById("draft");
686
833
  const raw = draft.value.trim();
@@ -736,11 +883,22 @@
736
883
  document.getElementById("stop").addEventListener("click", () => {
737
884
  stopTurn();
738
885
  });
886
+ document.getElementById("steer").addEventListener("click", () => {
887
+ steerDraft().catch((err) => toast(err.message || String(err)));
888
+ });
739
889
  document.getElementById("thread").addEventListener("click", (event) => {
740
890
  const stop = event.target.closest("[data-live-stop]");
741
- if (!stop) return;
891
+ if (stop) {
892
+ event.preventDefault();
893
+ stopTurn(stop.getAttribute("data-live-stop"));
894
+ return;
895
+ }
896
+ const steer = event.target.closest("[data-live-steer]");
897
+ if (!steer) return;
742
898
  event.preventDefault();
743
- stopTurn(stop.getAttribute("data-live-stop"));
899
+ steerDraft(steer.getAttribute("data-live-steer")).catch((err) =>
900
+ toast(err.message || String(err)),
901
+ );
744
902
  });
745
903
  document.getElementById("draft").addEventListener("input", () => {
746
904
  fitDraft();
@@ -748,6 +906,18 @@
748
906
  });
749
907
  document.getElementById("draft").addEventListener("keydown", (event) => {
750
908
  const pop = document.getElementById("mention-pop");
909
+ if (
910
+ pop.hidden &&
911
+ event.key === "Enter" &&
912
+ !event.shiftKey &&
913
+ (event.metaKey || event.ctrlKey)
914
+ ) {
915
+ if (isBusy()) {
916
+ event.preventDefault();
917
+ steerDraft().catch((err) => toast(err.message || String(err)));
918
+ }
919
+ return;
920
+ }
751
921
  if (pop.hidden) return;
752
922
  if (event.key === "ArrowDown") {
753
923
  event.preventDefault();
@@ -64,13 +64,10 @@
64
64
  </div>
65
65
  <p class="main-now" id="main-now"></p>
66
66
  <div class="toggles">
67
- <span>Reasoning</span>
68
- <select id="reasoning">
69
- <option value="minimal">Minimal</option>
70
- <option value="low">Low</option>
71
- <option value="medium">Medium</option>
72
- <option value="high">High</option>
73
- </select>
67
+ <span id="reasoning-wrap">
68
+ <span data-i18n="reasoning">推理強度</span>
69
+ <select id="reasoning"></select>
70
+ </span>
74
71
  <label><input type="checkbox" id="fast" /> Fast</label>
75
72
  </div>
76
73
  <div class="panel-head" style="margin-top:1rem">
@@ -278,6 +275,51 @@
278
275
  )
279
276
  .join("");
280
277
  if (preferred && preferred.provider === psel.value) msel.value = preferred.model;
278
+ if (psel.id === "provider") fillReasoning();
279
+ }
280
+ function reasonLabel(value) {
281
+ if (value === "none") return t("reason.none");
282
+ if (value === "minimal") return t("reason.min");
283
+ if (value === "low") return t("reason.low");
284
+ if (value === "medium") return t("reason.mid");
285
+ if (value === "high") return t("reason.high");
286
+ if (value === "xhigh") return t("reason.xhigh");
287
+ if (value === "max") return t("reason.max");
288
+ return value || "";
289
+ }
290
+ function fillReasoning() {
291
+ const sel = document.getElementById("reasoning");
292
+ const wrap = document.getElementById("reasoning-wrap");
293
+ const ref = currentRef();
294
+ const group = pickerGroups().find((p) => p.id === (ref && ref.provider));
295
+ const model = ((group && group.models) || []).find(
296
+ (m) => m.id === (ref && ref.model),
297
+ );
298
+ const spec = (model && model.reasoning) || null;
299
+ const raw = spec && spec.supportedEfforts;
300
+ const choices = raw && raw.length
301
+ ? spec.mandatory
302
+ ? raw.filter((key) => key !== "none")
303
+ : raw.slice()
304
+ : [];
305
+ sel.innerHTML = choices
306
+ .map(
307
+ (value) =>
308
+ '<option value="' +
309
+ escapeHtml(value) +
310
+ '">' +
311
+ escapeHtml(reasonLabel(value)) +
312
+ "</option>",
313
+ )
314
+ .join("");
315
+ const want = data.reasoning;
316
+ if (want && choices.indexOf(want) >= 0) sel.value = want;
317
+ else if (spec && spec.defaultEffort && choices.indexOf(spec.defaultEffort) >= 0) {
318
+ sel.value = spec.defaultEffort;
319
+ } else if (choices.length) {
320
+ sel.value = choices[0];
321
+ }
322
+ if (wrap) wrap.hidden = !choices.length;
281
323
  }
282
324
  function currentRef() {
283
325
  const provider = document.getElementById("provider").value;
@@ -359,7 +401,7 @@
359
401
  }
360
402
  function renderPicker() {
361
403
  fillSelect(document.getElementById("provider"), pickerGroups(), data.default);
362
- document.getElementById("reasoning").value = data.reasoning || "medium";
404
+ fillReasoning();
363
405
  document.getElementById("fast").checked = Boolean(data.fast);
364
406
  const mainNow = document.getElementById("main-now");
365
407
  if (mainNow) {
@@ -696,6 +738,7 @@
696
738
  data.default,
697
739
  );
698
740
  });
741
+ document.getElementById("model").addEventListener("change", fillReasoning);
699
742
  document.getElementById("apply").addEventListener("click", () => applyPicker());
700
743
  document.getElementById("reset-aux").addEventListener("click", async () => {
701
744
  await applyPicker({ aux: {} });