misakanet 2.44.1 → 2.45.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/.codex-plugin/plugin.json +1 -1
- package/README.ja.md +170 -290
- package/README.md +142 -280
- package/README.zh-CN.md +197 -374
- package/SKILL.md +9 -4
- package/lib/client.js +373 -49
- package/package.json +1 -1
- package/skills/misakanet/SKILL.md +9 -4
package/lib/client.js
CHANGED
|
@@ -27,12 +27,14 @@
|
|
|
27
27
|
* screen, and the only surface from which a human can speak about it.
|
|
28
28
|
*
|
|
29
29
|
* Why the vote is worth this much care: `POST /api/helpful` is the **only** writer of the counter that
|
|
30
|
-
* `misakanet_me_events` reports as `lesson_found_helpful
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* `misakanet_me_events` reports as `lesson_found_helpful`. That event is a *hint*, not the rung: since
|
|
31
|
+
* 2026-10-16 a vote is capped below `E4` at every count (measured then: 3 of the 24 most-retrieved
|
|
32
|
+
* lessons carried any vote, 6 votes in total, 0 of 483 lessons at `E4`), because the person shown the
|
|
33
|
+
* button did not ask the question, and the reuse the rung is about is observable at the transport
|
|
34
|
+
* layer instead. Three rules follow, and they explain several odd-looking details below:
|
|
33
35
|
*
|
|
34
36
|
* 1. **Never vote on the human's behalf.** No auto-vote on a successful build, no default selection,
|
|
35
|
-
* no pre-checked box:
|
|
37
|
+
* no pre-checked box: a vote means something only because a person chose to give it.
|
|
36
38
|
* 2. **Abstention is free and is the default.** Doing nothing is a first-class outcome — the buttons
|
|
37
39
|
* appear, and nothing nags if they are ignored.
|
|
38
40
|
* 3. **Say what each action sends.** 👍 posts the lesson id and nothing else. 👎 has to carry the search
|
|
@@ -126,17 +128,20 @@ window.__ModuleLoader__.load({
|
|
|
126
128
|
var en = {
|
|
127
129
|
"guide.description": "What this session asked, what came back, and what you filed",
|
|
128
130
|
"panel.asked": "What this session asked",
|
|
129
|
-
"panel.asked.none": "No MisakaNet search has run in this session yet.",
|
|
131
|
+
"panel.asked.none": "No MisakaNet search has run in this session yet — the entry point is the composer: type /misakanet, then what broke.",
|
|
130
132
|
"panel.reports": "Reports you filed",
|
|
131
133
|
"panel.reports.none": "Nothing filed from this session.",
|
|
132
134
|
"panel.trust": "How much these lessons are trusted",
|
|
133
|
-
"panel.trust.none": "No lesson surfaced yet.",
|
|
135
|
+
"panel.trust.none": "No lesson surfaced yet — the entry point is the composer: type /misakanet, then what broke.",
|
|
136
|
+
"panel.hint.open": "Open the search box over the composer",
|
|
137
|
+
"panel.hint.openLabel": "Open the search box",
|
|
138
|
+
"panel.hint.openTitle": "Opens the composer's own MisakaNet overlay, which carries the same instruction. It does not type into the composer: a view occupant receives props, and the draft is reachable only from a live session scope, so nothing here can put `/misakanet ` in the box for you.",
|
|
134
139
|
"panel.activity": "Your activity",
|
|
135
140
|
"panel.voice": "Voice",
|
|
136
141
|
"panel.voice.on": "Voice cues: on",
|
|
137
142
|
"panel.voice.off": "Voice cues: off",
|
|
138
143
|
"overlay.searching": "Searching…",
|
|
139
|
-
"overlay.empty": "
|
|
144
|
+
"overlay.empty": "After /misakanet, type what broke, e.g. /misakanet pip install timeout",
|
|
140
145
|
"overlay.none": "No lesson covers this yet. Ask the agent to file it (misakanet_submit_intake), or file it at misakanet.org — that is how the next person finds it here.",
|
|
141
146
|
"overlay.failed": "Search failed: {error} — the library is at misakanet.org; nothing was sent but the query.",
|
|
142
147
|
"overlay.footnote": "Reads are anonymous. This overlay sends only the query to misakanet.org; your votes and reports are the only other things a page ever sends.",
|
|
@@ -169,11 +174,15 @@ window.__ModuleLoader__.load({
|
|
|
169
174
|
"unit.votes": "votes",
|
|
170
175
|
"unit.report": "report",
|
|
171
176
|
"unit.reports": "reports",
|
|
177
|
+
"unit.reportFiled": "report filed",
|
|
178
|
+
"unit.reportsFiled": "reports filed",
|
|
179
|
+
"unit.retrieval": "retrieval",
|
|
180
|
+
"unit.retrievals": "retrievals",
|
|
172
181
|
"panel.asked.searching": "searching…",
|
|
173
182
|
"panel.asked.noMatch": "no lesson matched",
|
|
174
183
|
"panel.asked.top": "· top: ",
|
|
175
184
|
"panel.asked.noQuery": "(no query text)",
|
|
176
|
-
"panel.asked.nothingFound": "nothing
|
|
185
|
+
"panel.asked.nothingFound": "found nothing",
|
|
177
186
|
"panel.asked.voteHelpful": "Helpful",
|
|
178
187
|
"panel.asked.voteNotHelpful": "not helpful",
|
|
179
188
|
"panel.asked.voteNone": "not voted",
|
|
@@ -181,13 +190,35 @@ window.__ModuleLoader__.load({
|
|
|
181
190
|
"panel.asked.voteHint": "Same action as the verdict beside the assistant message, without scrolling back to it. Sends this lesson's id and one constant label (`dsh-web`); nothing else.",
|
|
182
191
|
"panel.asked.voteFailed": "could not reach misakanet.org — nothing was recorded",
|
|
183
192
|
"panel.asked.voteOne": "1 human confirmation — the reuse event now appears (E3: a reuse signal, not CI verification).",
|
|
184
|
-
"panel.asked.voteTwo": "2 human confirmations —
|
|
185
|
-
"panel.asked.voteMore": "human confirmations —
|
|
193
|
+
"panel.asked.voteTwo": "2 human confirmations — recorded as a hint; the E4 rung comes from observable reuse, not a vote.",
|
|
194
|
+
"panel.asked.voteMore": "human confirmations — recorded as a hint; the E4 rung comes from observable reuse, not a vote.",
|
|
186
195
|
"panel.asked.noMatchTag": "no match",
|
|
187
196
|
"panel.asked.agentNext": "Hand this line to the agent:",
|
|
188
197
|
"panel.asked.agentNextCopy": "Copy the next step",
|
|
189
198
|
"panel.asked.agentNextText": "Please record this gap with the MisakaNet intake so the next person finds it: misakanet_submit_intake problem: ",
|
|
190
199
|
"panel.asked.agentNextWhy": "This page cannot file it: a report is re-checked by submitting the same text again, the server's dedup window is finite, and a page-triggered submit could open a second issue for one problem.",
|
|
200
|
+
// The retrieval ranking is the network's own observation, not this session's, and it is not a
|
|
201
|
+
// judgment. Every word is a lookup for the same reason the counters are — a zh reader has to get
|
|
202
|
+
// zh — and the one thing none of these strings may say is that the order is approval: the count
|
|
203
|
+
// is how often a search put that lesson first, which says nothing about whether it helped.
|
|
204
|
+
"panel.hot": "What the corpus returns most",
|
|
205
|
+
"panel.hot.loading": "reading the network's retrieval ranking…",
|
|
206
|
+
"panel.hot.none": "The analytics endpoint ranked no lesson, so this section shows nothing rather than a zero.",
|
|
207
|
+
"panel.hot.rule": "Each number is how many times a search ranked that lesson as its top hit — a retrieval count from the network's own analytics. It is an observed count, not a vote, not an approval, and not evidence that the lesson helped.",
|
|
208
|
+
"panel.hot.limit": "The {n} most-retrieved lessons — the endpoint returns more, so this list stops at {n}.",
|
|
209
|
+
"panel.hot.noCount": "the endpoint did not report a count",
|
|
210
|
+
// The coverage read answers the *other* half of the same question, and it must not read as a
|
|
211
|
+
// second version of the ranking above: section ⑥ counts what searches brought back, this counts
|
|
212
|
+
// what they did not find. Every visible word is a lookup for the same reason the counters are,
|
|
213
|
+
// and the one thing none of these strings may say is that the order is an opinion — it is a
|
|
214
|
+
// count of unanswered searches.
|
|
215
|
+
"panel.coverage": "The topics the corpus cannot answer yet",
|
|
216
|
+
"panel.coverage.loading": "reading the network's coverage board…",
|
|
217
|
+
"panel.coverage.none": "The coverage board ranked no topic, so this section shows nothing rather than a guess.",
|
|
218
|
+
"panel.coverage.rule": "The ranking above counts what searches brought back; this one counts what they did not find. Each row is a topic with the last 30 days' unanswered searches, read from the network's own coverage board, and the number of published lessons that match it.",
|
|
219
|
+
"panel.coverage.lessonsTpl": "{count} in the corpus · ",
|
|
220
|
+
"panel.coverage.unsolvedTpl": "{count} found nothing in 30 days",
|
|
221
|
+
"panel.coverage.totals": "{count} published in all · the topics below are the ones with the most unanswered searches",
|
|
191
222
|
"panel.counters": "this session · counters local to this browser · no account, nothing uploaded but your votes and reports",
|
|
192
223
|
"panel.report.title": "MisakaNet report",
|
|
193
224
|
"panel.report.coveredBy": "covered by ",
|
|
@@ -208,12 +239,9 @@ window.__ModuleLoader__.load({
|
|
|
208
239
|
"panel.trust.used": " · used ",
|
|
209
240
|
"panel.trust.external": "external reuse evidence: ",
|
|
210
241
|
"panel.trust.declared": "declared: ",
|
|
211
|
-
"panel.trust.
|
|
212
|
-
"panel.trust.footnote": "The level is the lesson's own stored evidence. The count is other people's confirmations, not yours: one makes the reuse event appear, the second is what agents read as E4.",
|
|
242
|
+
"panel.trust.footnote": "The level is the lesson's own stored evidence; the count is other people's votes, not yours — recorded as a hint, never as proof. Agents read E4 only from observable reuse, which a vote cannot confer.",
|
|
213
243
|
"panel.activity.session": "this session: ",
|
|
214
244
|
"panel.activity.browser": "on this browser: ",
|
|
215
|
-
"panel.activity.reused": " reused · ",
|
|
216
|
-
"panel.activity.filed": " filed · ",
|
|
217
245
|
"panel.activity.conversion": "conversion",
|
|
218
246
|
"panel.activity.scope": "The browser column survives a reload; the session column is rebuilt from this conversation's transcript and starts again in a new session.",
|
|
219
247
|
"panel.voice.cue": "The cue is the server's own choice for the last search (",
|
|
@@ -221,7 +249,7 @@ window.__ModuleLoader__.load({
|
|
|
221
249
|
"panel.voice.none": "No cue yet: the server names one on the first search of the session.",
|
|
222
250
|
"panel.voice.play": "Play “",
|
|
223
251
|
"panel.voice.hook": "The local hook is a different thing: it is installed with `--voice` and mutes with MISAKANET_VOICE=0, plays through this machine's own player, and can also raise a desktop notification. A page cannot read or change it, so this panel does not report its state.",
|
|
224
|
-
"panel.footnote": "Every number above comes from this conversation's own transcript rows plus one public GET per lesson (/api/helpful). The trust counts are other people's confirmations, not yours. Reloading the window loses the session rows; the browser counters stay. Receipts shown here are what the agent's own calls returned — a pending report is re-checked when the agent asks again, not by this page.",
|
|
252
|
+
"panel.footnote": "Every number above comes from this conversation's own transcript rows plus one public GET per lesson (/api/helpful), one read of the network's retrieval ranking (/api/analytics) and one read of its coverage board (/api/insights/lesson-coverage). The trust counts are other people's confirmations, not yours. Reloading the window loses the session rows; the browser counters stay. Receipts shown here are what the agent's own calls returned — a pending report is re-checked when the agent asks again, not by this page.",
|
|
225
253
|
"main.footnote": "The left column opens this page; the session-scoped counters live in the session's right-sidebar MisakaNet tab. The numbers here are what this browser has contributed.",
|
|
226
254
|
"tool.rawResult": "raw result",
|
|
227
255
|
"tool.readFailed": "result could not be read",
|
|
@@ -240,7 +268,23 @@ window.__ModuleLoader__.load({
|
|
|
240
268
|
"settings.invalidJson": "invalid JSON",
|
|
241
269
|
"settings.what.form": "form",
|
|
242
270
|
"settings.what.row": "row",
|
|
243
|
-
|
|
271
|
+
// The counter strip and the activity line below are the two places a reader meets these
|
|
272
|
+
// numbers, and every word between them is a lookup: the unit words are `unit.*`, the rest are
|
|
273
|
+
// one-slot templates. Both halves are needed, because where the words go differs — English
|
|
274
|
+
// writes the plural noun then the verb (`{count} surfaced`), a language that takes a measure
|
|
275
|
+
// word before its noun writes one phrase (`{n} {unit}命中过课程`). Concatenating either half at
|
|
276
|
+
// the call site is what made the Chinese panel read `0 with a lesson · 0 lessons surfaced`,
|
|
277
|
+
// and splitting the phrase across two slots later made it read `1 篇 篇被课程复用`.
|
|
278
|
+
"panel.strip.hitTpl": "{count} hit · ",
|
|
279
|
+
"panel.strip.missTpl": "{count} found nothing · ",
|
|
280
|
+
"panel.strip.surfacedTpl": "{count} surfaced · ",
|
|
281
|
+
"panel.strip.helpfulTpl": "({n} helpful) · ",
|
|
282
|
+
"panel.strip.convertedTpl": "({n} became a lesson)",
|
|
283
|
+
"panel.activity.reusedTpl": "{count} reused · ",
|
|
284
|
+
"panel.activity.reportsTpl": "{count} · ",
|
|
285
|
+
"panel.activity.filedTpl": "{count}",
|
|
286
|
+
"unit.conversion": "conversion",
|
|
287
|
+
"unit.conversions": "conversions",
|
|
244
288
|
"footer.session": "session: ",
|
|
245
289
|
"footer.actionHint": "Copy the MisakaNet summary of the session that last used it",
|
|
246
290
|
"settings.copyPatch": "Copy patch snippet",
|
|
@@ -250,17 +294,20 @@ window.__ModuleLoader__.load({
|
|
|
250
294
|
var zh = {
|
|
251
295
|
"guide.description": "本会话问过什么、拿回了什么、你报了什么",
|
|
252
296
|
"panel.asked": "本会话问过什么",
|
|
253
|
-
"panel.asked.none": "本会话还没有发起过 MisakaNet
|
|
297
|
+
"panel.asked.none": "本会话还没有发起过 MisakaNet 检索 —— 入口就在输入框:先打 /misakanet,再写清楚出了什么问题。",
|
|
254
298
|
"panel.reports": "你报过的问题",
|
|
255
299
|
"panel.reports.none": "本会话没有报过任何问题。",
|
|
256
300
|
"panel.trust": "这些课程的可信度",
|
|
257
|
-
"panel.trust.none": "
|
|
301
|
+
"panel.trust.none": "还没有命中过课程 —— 入口就在输入框:先打 /misakanet,再写清楚出了什么问题。",
|
|
302
|
+
"panel.hint.open": "在输入框上方打开检索框",
|
|
303
|
+
"panel.hint.openLabel": "打开检索框",
|
|
304
|
+
"panel.hint.openTitle": "打开输入框自己的 MisakaNet 浮层,里面写着同样的用法。它不会替你在输入框里打字:视图组件只拿得到 props,草稿只有活的 session 作用域才能改,所以从这里没办法替你填入「/misakanet 」。",
|
|
258
305
|
"panel.activity": "你的活动",
|
|
259
306
|
"panel.voice": "语音",
|
|
260
307
|
"panel.voice.on": "语音提示:开",
|
|
261
308
|
"panel.voice.off": "语音提示:关",
|
|
262
309
|
"overlay.searching": "检索中……",
|
|
263
|
-
"overlay.empty": "在 /misakanet 后面写清楚出了什么问题,例如 /misakanet pip install
|
|
310
|
+
"overlay.empty": "在 /misakanet 后面写清楚出了什么问题,例如 /misakanet pip install 超时。",
|
|
264
311
|
"overlay.none": "还没有课程覆盖这个问题。可以让 agent 报料(misakanet_submit_intake),或直接去 misakanet.org 提交 —— 下一个人就能在这里搜到它。",
|
|
265
312
|
"overlay.failed": "检索失败:{error} —— 课程库在 misakanet.org;这次只发出了查询本身。",
|
|
266
313
|
"overlay.footnote": "读取是匿名的。这个浮层只把查询发给 misakanet.org;除此之外,页面只会发出你的投票与报料。",
|
|
@@ -293,11 +340,15 @@ window.__ModuleLoader__.load({
|
|
|
293
340
|
"unit.votes": "票",
|
|
294
341
|
"unit.report": "条报料",
|
|
295
342
|
"unit.reports": "条报料",
|
|
343
|
+
"unit.reportFiled": "条报料",
|
|
344
|
+
"unit.reportsFiled": "条报料",
|
|
345
|
+
"unit.retrieval": "次检索",
|
|
346
|
+
"unit.retrievals": "次检索",
|
|
296
347
|
"panel.asked.searching": "检索中……",
|
|
297
348
|
"panel.asked.noMatch": "没有命中任何课程",
|
|
298
349
|
"panel.asked.top": "· 最佳:",
|
|
299
350
|
"panel.asked.noQuery": "(没记下查询原文)",
|
|
300
|
-
"panel.asked.nothingFound": "
|
|
351
|
+
"panel.asked.nothingFound": "没查到结果",
|
|
301
352
|
"panel.asked.voteHelpful": "有用",
|
|
302
353
|
"panel.asked.voteNotHelpful": "没用",
|
|
303
354
|
"panel.asked.voteNone": "未投票",
|
|
@@ -305,13 +356,26 @@ window.__ModuleLoader__.load({
|
|
|
305
356
|
"panel.asked.voteHint": "与助手消息旁的裁决行是同一个动作,不用再往回滚。只发这篇课程的 id 和一个固定标签(`dsh-web`),不发别的。",
|
|
306
357
|
"panel.asked.voteFailed": "连不上 misakanet.org —— 这次没有记下任何东西",
|
|
307
358
|
"panel.asked.voteOne": "1 个人工确认 —— 复用事件现在出现了(E3:复用信号,不是 CI 验证)。",
|
|
308
|
-
"panel.asked.voteTwo": "2 个人工确认 ——
|
|
309
|
-
"panel.asked.voteMore": "个人工确认 ——
|
|
359
|
+
"panel.asked.voteTwo": "2 个人工确认 —— 只记为提示,不算证据:E4 那一档来自可观测的复用,投票给不了。",
|
|
360
|
+
"panel.asked.voteMore": "个人工确认 —— 只记为提示,不算证据:E4 那一档来自可观测的复用,投票给不了。",
|
|
310
361
|
"panel.asked.noMatchTag": "无命中",
|
|
311
362
|
"panel.asked.agentNext": "把这行交给 agent:",
|
|
312
363
|
"panel.asked.agentNextCopy": "复制下一步",
|
|
313
364
|
"panel.asked.agentNextText": "请用 MisakaNet 报料记下这个缺口,让下一个人能搜到:misakanet_submit_intake problem: ",
|
|
314
365
|
"panel.asked.agentNextWhy": "这个页面不能替你报:报料的复查方式是原样再提交一次,而服务端的去重窗口是有限的 —— 页面触发的提交可能为同一个问题开出第二个 issue。",
|
|
366
|
+
"panel.hot": "全网返回最多的课程",
|
|
367
|
+
"panel.hot.loading": "正在读全网的检索排行……",
|
|
368
|
+
"panel.hot.none": "统计端点没有排出任何课程 —— 这一节宁可什么都不显示,也不编造一个 0。",
|
|
369
|
+
"panel.hot.rule": "每个数字是这篇课程被检索结果排到首位的次数 —— 来自网络自己的统计端点。它是一个观测到的计数:不是投票,不是认可,也不代表这篇课程真的帮上了忙。",
|
|
370
|
+
"panel.hot.limit": "检索次数最多的 {n} 篇课程 —— 端点返回的更多,这里只列到第 {n} 篇。",
|
|
371
|
+
"panel.hot.noCount": "端点没有给出次数",
|
|
372
|
+
"panel.coverage": "语料还答不上来的主题",
|
|
373
|
+
"panel.coverage.loading": "正在读网络的覆盖看板……",
|
|
374
|
+
"panel.coverage.none": "覆盖看板没有排出任何主题 —— 这一节宁可什么都不显示,也不猜。",
|
|
375
|
+
"panel.coverage.rule": "上面那一节数的是检索带回来了什么;这一节数的是没找到什么。每一行是一个主题,配最近 30 天里没有拿到答案的检索次数(读的是网络自己的覆盖看板),以及语料里匹配该主题的已发布课程数。",
|
|
376
|
+
"panel.coverage.lessonsTpl": "{n} {unit}在库 · ",
|
|
377
|
+
"panel.coverage.unsolvedTpl": "30 天内 {n} {unit}没有答案",
|
|
378
|
+
"panel.coverage.totals": "{n} {unit}已发布 · 下面是没有答案的检索最多的主题",
|
|
315
379
|
"panel.counters": "本会话 · 计数只存在本浏览器 · 没有账号,除了你的投票与报料,不上传任何东西",
|
|
316
380
|
"panel.report.title": "MisakaNet 报料",
|
|
317
381
|
"panel.report.coveredBy": "已被 ",
|
|
@@ -332,12 +396,9 @@ window.__ModuleLoader__.load({
|
|
|
332
396
|
"panel.trust.used": " · 已复用 ",
|
|
333
397
|
"panel.trust.external": "外部复用证据:",
|
|
334
398
|
"panel.trust.declared": "课程自述:",
|
|
335
|
-
"panel.trust.
|
|
336
|
-
"panel.trust.footnote": "等级来自课程自己存的证据;计数是别人的确认,不是你的:前者让复用事件出现,后者才是 agent 读到的 E4。",
|
|
399
|
+
"panel.trust.footnote": "等级来自课程自己存的证据;计数是别人的投票,不是你的 —— 只记为提示,不算证据。agent 读到的 E4 只来自可观测的复用,投票给不了。",
|
|
337
400
|
"panel.activity.session": "本会话:",
|
|
338
401
|
"panel.activity.browser": "本浏览器:",
|
|
339
|
-
"panel.activity.reused": " 次复用 · ",
|
|
340
|
-
"panel.activity.filed": " 条报料 · ",
|
|
341
402
|
"panel.activity.conversion": "转化",
|
|
342
403
|
"panel.activity.scope": "浏览器那一列能跨刷新保留;会话那一列由本次对话的转录重建,换一个会话就从零开始。",
|
|
343
404
|
"panel.voice.cue": "提示音是服务端为上一次检索自己选的(",
|
|
@@ -345,7 +406,7 @@ window.__ModuleLoader__.load({
|
|
|
345
406
|
"panel.voice.none": "还没有提示音:服务端会在本会话第一次检索时给出。",
|
|
346
407
|
"panel.voice.play": "播放“",
|
|
347
408
|
"panel.voice.hook": "本地钩子是另一回事:用 --voice 安装,用 MISAKANET_VOICE=0 静音,通过本机自己的播放器发声,还能弹桌面通知。页面读不到也改不了它,所以这个面板不报告它的状态。",
|
|
348
|
-
"panel.footnote": "上面的数字都来自本次对话自己的转录,外加每篇课程一次公开 GET(/api/helpful)。可信计数是别人的确认,不是你自己的。重载窗口会丢掉会话行,浏览器计数会留下。这里的回执是 agent 自己调用返回的东西 —— 待处理报料会在 agent 再次提问时复查,而不是这个页面。",
|
|
409
|
+
"panel.footnote": "上面的数字都来自本次对话自己的转录,外加每篇课程一次公开 GET(/api/helpful)、一次对全网检索排行的读取(/api/analytics)和一次对覆盖看板的读取(/api/insights/lesson-coverage)。可信计数是别人的确认,不是你自己的。重载窗口会丢掉会话行,浏览器计数会留下。这里的回执是 agent 自己调用返回的东西 —— 待处理报料会在 agent 再次提问时复查,而不是这个页面。",
|
|
349
410
|
"main.footnote": "左栏打开的就是这一页;会话级的计数在右栏的 MisakaNet 标签里。这里的数字是本浏览器贡献的部分。",
|
|
350
411
|
"tool.rawResult": "原始结果",
|
|
351
412
|
"tool.readFailed": "结果读不出来",
|
|
@@ -364,7 +425,16 @@ window.__ModuleLoader__.load({
|
|
|
364
425
|
"settings.invalidJson": "JSON 不合法",
|
|
365
426
|
"settings.what.form": "表单",
|
|
366
427
|
"settings.what.row": "条目",
|
|
367
|
-
"panel.
|
|
428
|
+
"panel.strip.hitTpl": "{n} {unit}命中过课程 · ",
|
|
429
|
+
"panel.strip.missTpl": "{n} {unit}没命中 · ",
|
|
430
|
+
"panel.strip.surfacedTpl": "{n} {unit}命中过课程 · ",
|
|
431
|
+
"panel.strip.helpfulTpl": "({n} 有用) · ",
|
|
432
|
+
"panel.strip.convertedTpl": "({n} 转化为课程)",
|
|
433
|
+
"panel.activity.reusedTpl": "{n} {unit}被课程复用 · ",
|
|
434
|
+
"panel.activity.reportsTpl": "{n} {unit} · ",
|
|
435
|
+
"panel.activity.filedTpl": "{n} {unit}",
|
|
436
|
+
"unit.conversion": "次转化",
|
|
437
|
+
"unit.conversions": "次转化",
|
|
368
438
|
"footer.session": "会话:",
|
|
369
439
|
"footer.actionHint": "复制最近一次使用 MisakaNet 的那个会话的摘要",
|
|
370
440
|
"settings.copyPatch": "复制 patch 片段",
|
|
@@ -620,6 +690,38 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
620
690
|
var OVERLAY_LIMIT = 5;
|
|
621
691
|
var OVERLAY_TIMEOUT_MS = 8000;
|
|
622
692
|
|
|
693
|
+
/**
|
|
694
|
+
* How many rows section ⑥ shows of `/api/analytics`'s `top_lessons`.
|
|
695
|
+
*
|
|
696
|
+
* The owner asked for five and the endpoint returns ten, so the list is capped — and the cap is
|
|
697
|
+
* *printed* by the section (`panel.hot.limit`) rather than applied silently, because a reader who
|
|
698
|
+
* cannot see where a ranking was cut cannot tell it from a ranking that ended.
|
|
699
|
+
*/
|
|
700
|
+
var HOT_LESSON_LIMIT = 5;
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Section ⑦'s target, and the three reads it was chosen over.
|
|
704
|
+
*
|
|
705
|
+
* `/api/insights/lesson-coverage` is the other half of section ⑥'s question, not a second version
|
|
706
|
+
* of it: ⑥ borrows `/api/analytics` to rank *retrievals* — which lessons searches brought back —
|
|
707
|
+
* while this ranks *unmet demand*: `gaps` is ordered by `unsolved30d`, the searches in the last 30
|
|
708
|
+
* days that ended with no usable answer, and each row carries `lessonCount`, the published lessons
|
|
709
|
+
* that do match that topic. A reader who sees both should never think they are two orderings of the
|
|
710
|
+
* same number, which is what `panel.coverage.rule` says out loud.
|
|
711
|
+
*
|
|
712
|
+
* Of the four candidate reads measured live 2026-10-16 (all four answering
|
|
713
|
+
* `access-control-allow-origin: *`, so no server change is needed), this is the only one that both
|
|
714
|
+
* ranks something and says how much knowledge stands behind it. The others: `/api/insights/demand-board`
|
|
715
|
+
* is 146 bytes of family counters with no lesson counts in it; `/api/search-signals/stats` is 705 rows
|
|
716
|
+
* of `{solved, created_at}` — one per search, no lesson id — an unranked event log; `/api/lessons` is
|
|
717
|
+
* the corpus with no retrieval signal in any field, so any "hottest" order over it would be invented
|
|
718
|
+
* here. None of the four exposes a per-lesson retrieval count, which is why ⑥ and ⑦ split the
|
|
719
|
+
* question the way they do: ⑥ has the lessons and no topics, ⑦ has the topics and no individual
|
|
720
|
+
* lessons. The finest grain ⑦ can honestly speak at is the task family.
|
|
721
|
+
*/
|
|
722
|
+
var COVERAGE_ENDPOINT = "https://misakanet.org/api/insights/lesson-coverage";
|
|
723
|
+
var COVERAGE_LIMIT = 5;
|
|
724
|
+
|
|
623
725
|
/**
|
|
624
726
|
* The label every vote from this browser half carries: what cast it, not who.
|
|
625
727
|
*
|
|
@@ -959,11 +1061,21 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
959
1061
|
*
|
|
960
1062
|
* `many` is explicit rather than `singular + "s"` because English is not regular: the first
|
|
961
1063
|
* version of this helper printed "2 searchs", which its own verification run showed and the
|
|
962
|
-
* commit message then misquoted.
|
|
963
|
-
*
|
|
1064
|
+
* commit message then misquoted.
|
|
1065
|
+
*
|
|
1066
|
+
* The fallback keeps `many` optional, but it may only ever *derive an English plural*. A unit
|
|
1067
|
+
* that is not an English word — every `unit.*` key holds a Chinese one under zh — does not
|
|
1068
|
+
* pluralize with a trailing "s", so appending one did not make the count wrong in a way a reader
|
|
1069
|
+
* could parse: it made `count(0, "票")` print `"票s"` and `count(0, "转化")` print `"转化s"`,
|
|
1070
|
+
* which is the screenshot this file was fixed from. A unit that is not `[A-Za-z]…` therefore
|
|
1071
|
+
* keeps its singular in the plural slot, and the guard is the *character class*, not a list of
|
|
1072
|
+
* nouns: a new unit in a language this helper has never seen cannot grow an English plural.
|
|
1073
|
+
* Where English itself is irregular the call site still passes `many`, because no default can
|
|
1074
|
+
* know "search" wants "searches".
|
|
964
1075
|
*/
|
|
965
1076
|
function count(n, singular, many) {
|
|
966
|
-
|
|
1077
|
+
var plural = many || (/^[A-Za-z][A-Za-z '\u2019-]*$/.test(singular) ? singular + "s" : singular);
|
|
1078
|
+
return n + " " + (n === 1 ? singular : plural);
|
|
967
1079
|
}
|
|
968
1080
|
|
|
969
1081
|
/** mm:ss for today, else the date — the event's own time, never the render time. */
|
|
@@ -1218,7 +1330,9 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1218
1330
|
* counters — the review surface, as opposed to the two judging/visibility rows in the transcript.
|
|
1219
1331
|
*
|
|
1220
1332
|
* Every number here has a named source: this session's own log (the rows above), the browser's own
|
|
1221
|
-
* counters, one public `GET /api/helpful` per lesson,
|
|
1333
|
+
* counters, one public `GET /api/helpful` per lesson, one read of the network's retrieval ranking
|
|
1334
|
+
* (`GET /api/analytics`, section ⑥), one read of its coverage board
|
|
1335
|
+
* (`/api/insights/lesson-coverage`, section ⑦), and nothing else. The limits are printed in
|
|
1222
1336
|
* the panel rather than hidden: a reload loses the log, and the local voice hook is not ours to
|
|
1223
1337
|
* read.
|
|
1224
1338
|
*/
|
|
@@ -1237,6 +1351,19 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1237
1351
|
var voteState = react.useState({});
|
|
1238
1352
|
var panelVotes = voteState[0];
|
|
1239
1353
|
var setPanelVotes = voteState[1];
|
|
1354
|
+
// The retrieval read's own state, and deliberately a third value: `undefined` is "not answered
|
|
1355
|
+
// yet", `null` is "asked and there is no ranking". Collapsing them would make the first paint
|
|
1356
|
+
// say the endpoint ranked nothing, which is a claim the page has not earned yet — and it would
|
|
1357
|
+
// leave no way to tell an empty ranking from one still in flight.
|
|
1358
|
+
var hotState = react.useState(undefined);
|
|
1359
|
+
var hot = hotState[0];
|
|
1360
|
+
var setHot = hotState[1];
|
|
1361
|
+
// Section ⑦'s own state, and the same third value for the same reason: `undefined` is "not
|
|
1362
|
+
// answered yet", `null` is "asked and there is no ranking". Collapsing them would make the first
|
|
1363
|
+
// paint say the board ranked nothing, which is a claim the page has not earned yet.
|
|
1364
|
+
var coverageState = react.useState(undefined);
|
|
1365
|
+
var coverage = coverageState[0];
|
|
1366
|
+
var setCoverage = coverageState[1];
|
|
1240
1367
|
|
|
1241
1368
|
react.useEffect(function () {
|
|
1242
1369
|
return subscribe(sessionId, function () {
|
|
@@ -1293,6 +1420,82 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1293
1420
|
}
|
|
1294
1421
|
}, [revisionState[0], lessons.length]);
|
|
1295
1422
|
|
|
1423
|
+
// ⑥ the network's most-retrieved lessons, read once per mount.
|
|
1424
|
+
//
|
|
1425
|
+
// The owner asked to see the corpus's hottest lessons without waiting for an agent to surface
|
|
1426
|
+
// one, and the ranking that needs already exists: `GET /api/analytics` — public, `no-store`, and
|
|
1427
|
+
// `access-control-allow-origin: *` (measured 2026-10-16) — carries `top_lessons`, one
|
|
1428
|
+
// `{lesson_id, count}` per lesson, ordered by how often that lesson came back as a search's top
|
|
1429
|
+
// hit.
|
|
1430
|
+
//
|
|
1431
|
+
// **Why this endpoint and not the coverage board** (`/api/insights/lesson-coverage`, the read
|
|
1432
|
+
// PR #3351 adds): the two rank different things and both are honest. The coverage board ranks
|
|
1433
|
+
// *topics* by unmet demand (`unsolved30d`, searches that ended with no usable answer) and says
|
|
1434
|
+
// how many lessons match each topic; it carries no per-lesson retrieval count at all, so a
|
|
1435
|
+
// "hottest lesson" list read from it would be invented here. This endpoint ranks retrievals and
|
|
1436
|
+
// says nothing about whether the lesson helped — a price the section states out loud rather than
|
|
1437
|
+
// dressing the count as a verdict.
|
|
1438
|
+
//
|
|
1439
|
+
// **What is deliberately not rendered: `top_searches`**, the same payload's unfiltered query
|
|
1440
|
+
// list. On 2026-10-16 it led with `pip install timeout` and `Error response from daemon: exit
|
|
1441
|
+
// code 137`, then with `npm run build`, `init` and `start` — generic strings a build tool or a
|
|
1442
|
+
// shell prompt emits, not failure topics. It also carries no lesson link, so it cannot be paired
|
|
1443
|
+
// with the answer that satisfied it. Rendering it as "what people ask" would put noise on the
|
|
1444
|
+
// panel at the same weight as knowledge, so this section shows the ranking the payload really
|
|
1445
|
+
// provides and leaves the query list alone. Nothing here averages it in.
|
|
1446
|
+
//
|
|
1447
|
+
// The read is scheduled, never awaited on the render path: the panel's first paint must not wait
|
|
1448
|
+
// on misakanet.org. `askedHot` is the same once-only guard the per-lesson `GET /api/helpful`
|
|
1449
|
+
// uses, because the effect can re-run and a public endpoint should not be asked twice for one
|
|
1450
|
+
// answer.
|
|
1451
|
+
var askedHot = react.useRef(false);
|
|
1452
|
+
react.useEffect(function () {
|
|
1453
|
+
if (askedHot.current) return;
|
|
1454
|
+
askedHot.current = true;
|
|
1455
|
+
fetch(API + "/api/analytics")
|
|
1456
|
+
.then(function (response) { return response.ok ? response.json() : null; })
|
|
1457
|
+
.then(function (body) {
|
|
1458
|
+
var ranked = body && Array.isArray(body.top_lessons) ? body.top_lessons : null;
|
|
1459
|
+
// An absent or empty `top_lessons` is "no ranking", never a zero: a manufactured `0`
|
|
1460
|
+
// would name a lesson the endpoint did not rank.
|
|
1461
|
+
setHot(ranked && ranked.length ? ranked : null);
|
|
1462
|
+
})
|
|
1463
|
+
.catch(function () { setHot(null); });
|
|
1464
|
+
}, []);
|
|
1465
|
+
|
|
1466
|
+
// ⑦ what the corpus cannot answer yet, read once per mount.
|
|
1467
|
+
//
|
|
1468
|
+
// The complement of ⑥, and deliberately not a re-sort of it. ⑥ ranks *lessons by retrievals*
|
|
1469
|
+
// (`/api/analytics`'s `top_lessons`); this ranks *topics by unanswered searches*
|
|
1470
|
+
// (`/api/insights/lesson-coverage`'s `gaps`, `unsolved30d` descending) and prints how many
|
|
1471
|
+
// published lessons match each topic. Two different measurements of two different things: what
|
|
1472
|
+
// the corpus returns most, and where it has nothing to return. `panel.coverage.rule` states the
|
|
1473
|
+
// difference where the reader meets it, because a panel with two rankings that never says how
|
|
1474
|
+
// they differ invites the reader to assume one is a re-sort of the other.
|
|
1475
|
+
//
|
|
1476
|
+
// Why this endpoint rather than `/api/analytics`: the retrieval ranking carries no topic and no
|
|
1477
|
+
// gap count, and none of the four reads measured on 2026-10-16 carries a per-lesson retrieval
|
|
1478
|
+
// count *and* a topic. `gaps` is the only public ranking of unmet demand, and `lessonCount` is
|
|
1479
|
+
// the only public statement of how much knowledge stands behind each topic.
|
|
1480
|
+
//
|
|
1481
|
+
// Scheduled, never awaited on the render path — the first paint must not wait on misakanet.org —
|
|
1482
|
+
// and guarded by a ref for the same reason as ⑥: the effect can re-run and a public endpoint
|
|
1483
|
+
// should not be asked twice for one answer.
|
|
1484
|
+
var askedCoverage = react.useRef(false);
|
|
1485
|
+
react.useEffect(function () {
|
|
1486
|
+
if (askedCoverage.current) return;
|
|
1487
|
+
askedCoverage.current = true;
|
|
1488
|
+
fetch(COVERAGE_ENDPOINT)
|
|
1489
|
+
.then(function (response) { return response.ok ? response.json() : null; })
|
|
1490
|
+
.then(function (body) {
|
|
1491
|
+
var ranked = body && body.success && Array.isArray(body.gaps) ? body.gaps : null;
|
|
1492
|
+
// An absent or empty `gaps` is "no ranking", never a list of zeroes: the totals line
|
|
1493
|
+
// would otherwise describe a corpus standing behind a ranking that does not exist.
|
|
1494
|
+
setCoverage(ranked && ranked.length ? body : null);
|
|
1495
|
+
})
|
|
1496
|
+
.catch(function () { setCoverage(null); });
|
|
1497
|
+
}, []);
|
|
1498
|
+
|
|
1296
1499
|
var rows = [];
|
|
1297
1500
|
// The same mark as the plugin-list icon and the sidebar chip: the conversation ring has no icon
|
|
1298
1501
|
// channel, so the panel body is the one place the tab can show what it belongs to.
|
|
@@ -1301,11 +1504,17 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1301
1504
|
rows.push(h("div", { key: "scope", style: DIM },
|
|
1302
1505
|
T("panel.counters")));
|
|
1303
1506
|
|
|
1507
|
+
// The strip and the activity line below are the two places a reader meets these counters, and
|
|
1508
|
+
// every word between the numbers is a lookup. Hard-coding it at the call site is what made the
|
|
1509
|
+
// Chinese panel read `0 with a lesson · 0 lessons surfaced · 0 became a lesson` — the numbers
|
|
1510
|
+
// were right and the sentence was English.
|
|
1304
1511
|
rows.push(h("div", { key: "strip", style: Object.assign({}, BOX, { marginTop: "8px" }) },
|
|
1305
|
-
count(searches.length, T("unit.search"), T("unit.searches")) + " · " +
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
count(
|
|
1512
|
+
count(searches.length, T("unit.search"), T("unit.searches")) + " · " +
|
|
1513
|
+
T("panel.strip.hitTpl", { count: count(withLesson, T("unit.lesson"), T("unit.lessons")), n: withLesson, unit: T("unit.lessons") }) +
|
|
1514
|
+
T("panel.strip.missTpl", { count: count(noMatch, T("unit.lesson"), T("unit.lessons")), n: noMatch, unit: T("unit.lessons") }) +
|
|
1515
|
+
T("panel.strip.surfacedTpl", { count: count(lessons.length, T("unit.lesson"), T("unit.lessons")), n: lessons.length, unit: T("unit.lessons") }) +
|
|
1516
|
+
count(votes.length, T("unit.vote"), T("unit.votes")) + " " + T("panel.strip.helpfulTpl", { count: count(helpful, T("unit.vote"), T("unit.votes")), n: helpful, unit: T("unit.votes") }) +
|
|
1517
|
+
count(intakes.length, T("unit.report"), T("unit.reports")) + " " + T("panel.strip.convertedTpl", { count: count(converted, T("unit.report"), T("unit.reports")), n: converted, unit: T("unit.reports") })));
|
|
1309
1518
|
|
|
1310
1519
|
// ① what this session asked
|
|
1311
1520
|
//
|
|
@@ -1352,9 +1561,47 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1352
1561
|
);
|
|
1353
1562
|
}
|
|
1354
1563
|
|
|
1564
|
+
/**
|
|
1565
|
+
* The entry point, which the panel is the first place a person looks for and the last place
|
|
1566
|
+
* it used to mention.
|
|
1567
|
+
*
|
|
1568
|
+
* The real entry is the composer's `/misakanet` source, and the empty states below said only
|
|
1569
|
+
* what was missing (`panel.asked.none`, `panel.trust.none`). What was checked before settling
|
|
1570
|
+
* for a hint plus this one button:
|
|
1571
|
+
*
|
|
1572
|
+
* * **Prefilling the composer is out of reach.** The draft lives behind
|
|
1573
|
+
* `ctx.conversation.input.for(actx).setDraft(text)` — a *session-scoped* service the panel
|
|
1574
|
+
* cannot hold: a view occupant is handed props, and the panel's `apply(ctx)` is the client
|
|
1575
|
+
* root. Reaching it would mean injecting a service this plugin does not declare, which on a
|
|
1576
|
+
* line without it is the dead-button case this file already refuses (see the deferred
|
|
1577
|
+
* `sidebarRightTabs` inject). So no prefilled `/misakanet ` and no text that claims one.
|
|
1578
|
+
* * **Opening the overlay is in reach**, because it is this module's own state: the card
|
|
1579
|
+
* registered on `conversation.input.overlay` renders from `overlayOf(sessionId)`, and the
|
|
1580
|
+
* panel receives that same session id. `runOverlaySearch(sessionId, "")` is the module's one
|
|
1581
|
+
* opener; it starts no request for an empty query and sends nothing — it just puts
|
|
1582
|
+
* `overlay.empty` in front of the person, where the instruction now is.
|
|
1583
|
+
*
|
|
1584
|
+
* The button is offered only with a session id (the `main` seat gets none): without one there is
|
|
1585
|
+
* no overlay to open, and a button that cannot do what its label says is worse than the text.
|
|
1586
|
+
*/
|
|
1587
|
+
function openOverlayFromPanel() {
|
|
1588
|
+
if (!sessionId) return;
|
|
1589
|
+
runOverlaySearch(sessionId, "");
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** The section's own empty-state sentence, plus the overlay opener when a session can take it. */
|
|
1593
|
+
function entryPointHint(key, message) {
|
|
1594
|
+
return h("div", { key: key, style: DIM },
|
|
1595
|
+
T(message) + " ",
|
|
1596
|
+
sessionId ? h("button", {
|
|
1597
|
+
type: "button", style: BUTTON, title: T("panel.hint.openTitle"),
|
|
1598
|
+
onClick: openOverlayFromPanel,
|
|
1599
|
+
}, T("panel.hint.openLabel")) : null);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1355
1602
|
rows.push(h("div", { key: "s1", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.asked")));
|
|
1356
1603
|
if (!searches.length) {
|
|
1357
|
-
rows.push(
|
|
1604
|
+
rows.push(entryPointHint("s1e", "panel.asked.none"));
|
|
1358
1605
|
} else {
|
|
1359
1606
|
var searchRows = searches.slice().reverse().slice(0, 20).map(function (entry) {
|
|
1360
1607
|
var key = "q" + entry.callId + entry.at;
|
|
@@ -1458,15 +1705,18 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1458
1705
|
// richer receipt (per-event types, `count`/`queries`/`sources`, its own `evidence`) but it is an
|
|
1459
1706
|
// MCP tool, reachable only through the MCP endpoint with a JSON-RPC `tools/call` — and this
|
|
1460
1707
|
// browser half is forbidden from making one (acceptance D4: the same endpoint files issues). The
|
|
1461
|
-
// count therefore carries the helpful-vote signal, and the
|
|
1462
|
-
//
|
|
1708
|
+
// count therefore carries the helpful-vote signal, and the observed distinct-client retrieval,
|
|
1709
|
+
// the regression citations and the declared-provenance hint the tool also aggregates are **not**
|
|
1710
|
+
// rendered. Nothing here is guessed: no field
|
|
1463
1711
|
// this endpoint does not return is displayed.
|
|
1464
1712
|
//
|
|
1465
|
-
// The count
|
|
1466
|
-
//
|
|
1713
|
+
// The count is the helpful-vote signal only, and since 2026-10-16 a vote is capped below E4 at
|
|
1714
|
+
// every count — no row here can claim the rung, which is also why the rule below is stated once.
|
|
1715
|
+
// (`E4` itself is observed elsewhere: `misakanet_me_events` counts distinct clients from
|
|
1716
|
+
// `search_signals`, which this browser half likewise cannot read.)
|
|
1467
1717
|
rows.push(h("div", { key: "s3", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.trust")));
|
|
1468
1718
|
if (!lessons.length) {
|
|
1469
|
-
rows.push(
|
|
1719
|
+
rows.push(entryPointHint("s3e", "panel.trust.none"));
|
|
1470
1720
|
} else {
|
|
1471
1721
|
// The rule once, not per row: it is the same sentence for every lesson, and repeating it
|
|
1472
1722
|
// four times buries the numbers it is there to explain.
|
|
@@ -1476,10 +1726,10 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1476
1726
|
var confirmations = trust[lesson.lessonId];
|
|
1477
1727
|
var counted = confirmations === undefined ? T("panel.trust.checking")
|
|
1478
1728
|
: (confirmations === null ? T("panel.trust.unavailable")
|
|
1479
|
-
: count(confirmations, T("panel.trust.human"))
|
|
1729
|
+
: count(confirmations, T("panel.trust.human")));
|
|
1480
1730
|
var declared = lesson.evidence || T("panel.trust.noLevel");
|
|
1481
1731
|
var external = h("span", { style: DIM },
|
|
1482
|
-
T("panel.trust.external") + counted
|
|
1732
|
+
T("panel.trust.external") + counted);
|
|
1483
1733
|
return h("div", { key: "l" + lesson.lessonId, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
1484
1734
|
h("div", null,
|
|
1485
1735
|
h("span", {}, lesson.title || lesson.lessonId),
|
|
@@ -1492,12 +1742,16 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1492
1742
|
// ④ your activity, two scopes
|
|
1493
1743
|
rows.push(h("div", { key: "s4", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.activity")));
|
|
1494
1744
|
rows.push(h("div", { key: "s4r", style: BOX },
|
|
1495
|
-
h("div", {}, T("panel.activity.session") + count(searches.length, "search", "searches") + " · " +
|
|
1496
|
-
T("panel.activity.
|
|
1497
|
-
count(
|
|
1745
|
+
h("div", {}, T("panel.activity.session") + count(searches.length, T("unit.search"), T("unit.searches")) + " · " +
|
|
1746
|
+
T("panel.activity.reusedTpl", { count: count(lessons.length, T("unit.lesson"), T("unit.lessons")), n: lessons.length, unit: T("unit.lessons") }) +
|
|
1747
|
+
count(votes.length, T("unit.vote"), T("unit.votes")) + " · " +
|
|
1748
|
+
T("panel.activity.reportsTpl", { count: count(intakes.length, T("unit.reportFiled"), T("unit.reportsFiled")), n: intakes.length, unit: T("unit.reportsFiled") }) +
|
|
1749
|
+
T("panel.activity.filedTpl", { count: count(converted, T("unit.conversion"), T("unit.conversions")), n: converted, unit: T("unit.conversions") })),
|
|
1498
1750
|
h("div", {}, T("panel.activity.browser") + count(stats.searches || 0, T("unit.search"), T("unit.searches")) + " · " +
|
|
1499
|
-
count(stats.lessonsReused || 0, "lesson")
|
|
1500
|
-
count(stats.
|
|
1751
|
+
T("panel.activity.reusedTpl", { count: count(stats.lessonsReused || 0, T("unit.lesson"), T("unit.lessons")), n: stats.lessonsReused || 0, unit: T("unit.lessons") }) +
|
|
1752
|
+
count(stats.votes || 0, T("unit.vote"), T("unit.votes")) + " · " +
|
|
1753
|
+
T("panel.activity.reportsTpl", { count: count(stats.reportsFiled || 0, T("unit.reportFiled"), T("unit.reportsFiled")), n: stats.reportsFiled || 0, unit: T("unit.reportsFiled") }) +
|
|
1754
|
+
T("panel.activity.filedTpl", { count: count(stats.reportsConverted || 0, T("unit.conversion"), T("unit.conversions")), n: stats.reportsConverted || 0, unit: T("unit.conversions") })),
|
|
1501
1755
|
h("div", { style: DIM }, T("panel.activity.scope"))));
|
|
1502
1756
|
|
|
1503
1757
|
// ⑤ voice
|
|
@@ -1522,6 +1776,76 @@ var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
|
1522
1776
|
: T("panel.voice.none")),
|
|
1523
1777
|
h("div", { style: DIM }, T("panel.voice.hook"))));
|
|
1524
1778
|
|
|
1779
|
+
// ⑥ the network's most-retrieved lessons
|
|
1780
|
+
//
|
|
1781
|
+
// Read-only, and about the whole network rather than this session: no lesson here had to be
|
|
1782
|
+
// surfaced by the agent first. There is no vote affordance in this section and none of its copy
|
|
1783
|
+
// says the count is approval — the number is an observed retrieval, and the rule states what it
|
|
1784
|
+
// does and does not mean before the first row.
|
|
1785
|
+
rows.push(h("div", { key: "s6", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.hot")));
|
|
1786
|
+
if (hot === undefined) {
|
|
1787
|
+
rows.push(h("div", { key: "s6w", style: DIM }, T("panel.hot.loading")));
|
|
1788
|
+
} else if (hot === null) {
|
|
1789
|
+
// The endpoint ranked nothing (or did not answer). Saying so beats printing five rows of
|
|
1790
|
+
// `0 retrievals`, which would name lessons the payload never ranked.
|
|
1791
|
+
rows.push(h("div", { key: "s6e", style: DIM }, T("panel.hot.none")));
|
|
1792
|
+
} else {
|
|
1793
|
+
rows.push(h("div", { key: "s6rule", style: DIM }, T("panel.hot.rule")));
|
|
1794
|
+
rows.push(h("div", { key: "s6l", style: DIM }, T("panel.hot.limit", { n: HOT_LESSON_LIMIT })));
|
|
1795
|
+
rows.push(h("div", { key: "s6r" }, hot.slice(0, HOT_LESSON_LIMIT).map(function (entry) {
|
|
1796
|
+
var path = String(entry && entry.lesson_id ? entry.lesson_id : "");
|
|
1797
|
+
var times = entry && typeof entry.count === "number" ? entry.count : null;
|
|
1798
|
+
return h("div", { key: "h" + path, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
1799
|
+
h("span", { style: { fontFamily: "monospace" } }, path),
|
|
1800
|
+
h("span", { style: DIM }, " · " + (times === null
|
|
1801
|
+
? T("panel.hot.noCount")
|
|
1802
|
+
: count(times, T("unit.retrieval"), T("unit.retrievals")))));
|
|
1803
|
+
})));
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// ⑦ the topics the corpus cannot answer yet
|
|
1807
|
+
//
|
|
1808
|
+
// Why ⑦ and not ⑥: `main` already numbers the retrieval ranking ⑥ (and owns `key: "s6"` plus the
|
|
1809
|
+
// `panel.hot` keys), so this section takes the next number and the `s7` key rather than sharing
|
|
1810
|
+
// one — two sections with the same React key is a duplicate-key defect, and two sections that
|
|
1811
|
+
// both claim to be ⑥ make the rule lines unreadable. Every key here is `s7*`, and the dictionary
|
|
1812
|
+
// family is `panel.coverage*`, so neither half of that identity is borrowed.
|
|
1813
|
+
//
|
|
1814
|
+
// Read-only, and about the whole network rather than this session: no topic here had to be
|
|
1815
|
+
// searched first. There is no vote affordance in this section and none of its copy says the order
|
|
1816
|
+
// is approval — `unsolved30d` counts unanswered searches, and the rule says what it counts before
|
|
1817
|
+
// the first row and how it differs from the retrieval ranking above.
|
|
1818
|
+
rows.push(h("div", { key: "s7", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.coverage")));
|
|
1819
|
+
if (coverage === undefined) {
|
|
1820
|
+
rows.push(h("div", { key: "s7w", style: DIM }, T("panel.coverage.loading")));
|
|
1821
|
+
} else if (coverage === null) {
|
|
1822
|
+
// The board ranked nothing (or did not answer). Saying so beats printing rows of `0`, which
|
|
1823
|
+
// would name topics the payload never ranked.
|
|
1824
|
+
rows.push(h("div", { key: "s7e", style: DIM }, T("panel.coverage.none")));
|
|
1825
|
+
} else {
|
|
1826
|
+
rows.push(h("div", { key: "s7rule", style: DIM }, T("panel.coverage.rule")));
|
|
1827
|
+
var coverageMetrics = coverage.metrics || {};
|
|
1828
|
+
if (typeof coverageMetrics.publishedLessons === "number") {
|
|
1829
|
+
rows.push(h("div", { key: "s7t", style: DIM },
|
|
1830
|
+
T("panel.coverage.totals", {
|
|
1831
|
+
count: count(coverageMetrics.publishedLessons, T("unit.lesson"), T("unit.lessons")),
|
|
1832
|
+
n: coverageMetrics.publishedLessons, unit: T("unit.lessons"),
|
|
1833
|
+
})));
|
|
1834
|
+
}
|
|
1835
|
+
rows.push(h("div", { key: "s7r" }, coverage.gaps.slice(0, COVERAGE_LIMIT).map(function (gap) {
|
|
1836
|
+
return h("div", { key: "f" + gap.taskFamily, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
1837
|
+
h("span", {}, gap.taskFamily),
|
|
1838
|
+
h("span", { style: DIM }, " " + T("panel.coverage.lessonsTpl", {
|
|
1839
|
+
count: count(gap.lessonCount, T("unit.lesson"), T("unit.lessons")),
|
|
1840
|
+
n: gap.lessonCount, unit: T("unit.lessons"),
|
|
1841
|
+
})),
|
|
1842
|
+
h("span", { style: DIM }, T("panel.coverage.unsolvedTpl", {
|
|
1843
|
+
count: count(gap.unsolved30d, T("unit.search"), T("unit.searches")),
|
|
1844
|
+
n: gap.unsolved30d, unit: T("unit.searches"),
|
|
1845
|
+
})));
|
|
1846
|
+
})));
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1525
1849
|
rows.push(h("div", { key: "foot", style: Object.assign({}, DIM, { marginTop: "12px" }) },
|
|
1526
1850
|
T("panel.footnote")));
|
|
1527
1851
|
|