dsh-mobile-plugin 0.5.28 → 0.5.29
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/extensions/catalog.json +10 -1
- package/extensions/magpie/agentcompact.js +104 -0
- package/extensions/magpie/app.css +4902 -0
- package/extensions/magpie/app.js +24474 -0
- package/extensions/magpie/autobackup.js +185 -0
- package/extensions/magpie/autoresetown.js +151 -0
- package/extensions/magpie/balfound.js +38 -0
- package/extensions/magpie/boot.js +23 -0
- package/extensions/magpie/cfgblocked.js +39 -0
- package/extensions/magpie/checkinnow.js +111 -0
- package/extensions/magpie/claudemode.js +92 -0
- package/extensions/magpie/compat.js +41 -0
- package/extensions/magpie/context.css +433 -0
- package/extensions/magpie/context.js +1576 -0
- package/extensions/magpie/ctxtext.js +223 -0
- package/extensions/magpie/fonts.js +42 -0
- package/extensions/magpie/i18n.js +18942 -0
- package/extensions/magpie/icons/ai21.svg +1 -0
- package/extensions/magpie/icons/ai302-color.svg +1 -0
- package/extensions/magpie/icons/ai360-color.svg +1 -0
- package/extensions/magpie/icons/aihubmix-color.svg +1 -0
- package/extensions/magpie/icons/aionui.svg +1 -0
- package/extensions/magpie/icons/air.svg +4 -0
- package/extensions/magpie/icons/alibaba-color.svg +1 -0
- package/extensions/magpie/icons/alibabacloud-color.svg +1 -0
- package/extensions/magpie/icons/alma.png +0 -0
- package/extensions/magpie/icons/anthropic.svg +1 -0
- package/extensions/magpie/icons/antigravity-color.svg +1 -0
- package/extensions/magpie/icons/anyscale-color.svg +1 -0
- package/extensions/magpie/icons/app-alma.png +0 -0
- package/extensions/magpie/icons/app-cc-switch.png +0 -0
- package/extensions/magpie/icons/apple-touch-icon.png +0 -0
- package/extensions/magpie/icons/aside.svg +28 -0
- package/extensions/magpie/icons/astrbot.svg +1 -0
- package/extensions/magpie/icons/atomcode.png +0 -0
- package/extensions/magpie/icons/aws.svg +1 -0
- package/extensions/magpie/icons/azure-color.svg +1 -0
- package/extensions/magpie/icons/azureai-color.svg +1 -0
- package/extensions/magpie/icons/baichuan-color.svg +1 -0
- package/extensions/magpie/icons/baidu-color.svg +1 -0
- package/extensions/magpie/icons/baiducloud-color.svg +1 -0
- package/extensions/magpie/icons/bailian-color.svg +1 -0
- package/extensions/magpie/icons/baseten.svg +1 -0
- package/extensions/magpie/icons/bedrock-color.svg +1 -0
- package/extensions/magpie/icons/burncloud-color.svg +1 -0
- package/extensions/magpie/icons/bytedance-color.svg +1 -0
- package/extensions/magpie/icons/cerebras.svg +1 -0
- package/extensions/magpie/icons/chatglm-color.svg +1 -0
- package/extensions/magpie/icons/cherryin-color.svg +1 -0
- package/extensions/magpie/icons/chutes.svg +1 -0
- package/extensions/magpie/icons/cindy.png +0 -0
- package/extensions/magpie/icons/claude-color.svg +1 -0
- package/extensions/magpie/icons/claudecode-color.svg +1 -0
- package/extensions/magpie/icons/cline.svg +1 -0
- package/extensions/magpie/icons/cloudflare-color.svg +1 -0
- package/extensions/magpie/icons/codebuddy-color.svg +1 -0
- package/extensions/magpie/icons/codex-color.svg +1 -0
- package/extensions/magpie/icons/cohere-color.svg +1 -0
- package/extensions/magpie/icons/cometapi-color.svg +1 -0
- package/extensions/magpie/icons/commandcode.svg +1 -0
- package/extensions/magpie/icons/copilot-color.svg +1 -0
- package/extensions/magpie/icons/coze.svg +1 -0
- package/extensions/magpie/icons/crush.png +0 -0
- package/extensions/magpie/icons/curl.svg +1 -0
- package/extensions/magpie/icons/cursor.svg +1 -0
- package/extensions/magpie/icons/deepinfra-color.svg +1 -0
- package/extensions/magpie/icons/deepmind-color.svg +1 -0
- package/extensions/magpie/icons/deepseek-color.svg +1 -0
- package/extensions/magpie/icons/deveco-color.svg +12 -0
- package/extensions/magpie/icons/devin.svg +1 -0
- package/extensions/magpie/icons/doubao-color.svg +1 -0
- package/extensions/magpie/icons/empryo-color.svg +1 -0
- package/extensions/magpie/icons/factory.svg +1 -0
- package/extensions/magpie/icons/favicon.svg +1 -0
- package/extensions/magpie/icons/featherless-color.svg +1 -0
- package/extensions/magpie/icons/fireworks-color.svg +1 -0
- package/extensions/magpie/icons/friendli.svg +1 -0
- package/extensions/magpie/icons/fx.svg +1 -0
- package/extensions/magpie/icons/gemini-color.svg +1 -0
- package/extensions/magpie/icons/geminicli-color.svg +1 -0
- package/extensions/magpie/icons/gemma-color.svg +1 -0
- package/extensions/magpie/icons/giteeai.svg +1 -0
- package/extensions/magpie/icons/github.svg +1 -0
- package/extensions/magpie/icons/githubcopilot.svg +1 -0
- package/extensions/magpie/icons/glama.svg +1 -0
- package/extensions/magpie/icons/gmicloud.svg +1 -0
- package/extensions/magpie/icons/google-color.svg +1 -0
- package/extensions/magpie/icons/googlecloud-color.svg +1 -0
- package/extensions/magpie/icons/goose.svg +1 -0
- package/extensions/magpie/icons/grok.svg +1 -0
- package/extensions/magpie/icons/groq.svg +1 -0
- package/extensions/magpie/icons/hanako.png +0 -0
- package/extensions/magpie/icons/hermes.svg +1 -0
- package/extensions/magpie/icons/huawei-color.svg +1 -0
- package/extensions/magpie/icons/huaweicloud-color.svg +1 -0
- package/extensions/magpie/icons/huggingface-color.svg +1 -0
- package/extensions/magpie/icons/hunyuan-color.svg +1 -0
- package/extensions/magpie/icons/hyperbolic-color.svg +1 -0
- package/extensions/magpie/icons/icon-512.png +0 -0
- package/extensions/magpie/icons/iflytekcloud-color.svg +1 -0
- package/extensions/magpie/icons/inference.svg +1 -0
- package/extensions/magpie/icons/infinigence-color.svg +1 -0
- package/extensions/magpie/icons/internlm-color.svg +1 -0
- package/extensions/magpie/icons/jcode.svg +1 -0
- package/extensions/magpie/icons/jina.svg +1 -0
- package/extensions/magpie/icons/kilo.svg +1 -0
- package/extensions/magpie/icons/kilocode.svg +1 -0
- package/extensions/magpie/icons/kimi-code-desktop.png +0 -0
- package/extensions/magpie/icons/kimi.svg +1 -0
- package/extensions/magpie/icons/kiro-color.svg +1 -0
- package/extensions/magpie/icons/kling-color.svg +1 -0
- package/extensions/magpie/icons/lambda.svg +1 -0
- package/extensions/magpie/icons/leptonai-color.svg +1 -0
- package/extensions/magpie/icons/liquid.svg +1 -0
- package/extensions/magpie/icons/liveagent-color.svg +1 -0
- package/extensions/magpie/icons/llmapi.svg +1 -0
- package/extensions/magpie/icons/lmstudio.svg +1 -0
- package/extensions/magpie/icons/lody.png +0 -0
- package/extensions/magpie/icons/longcat.svg +1 -0
- package/extensions/magpie/icons/magpie.svg +1 -0
- package/extensions/magpie/icons/meta-color.svg +1 -0
- package/extensions/magpie/icons/metaai-color.svg +1 -0
- package/extensions/magpie/icons/microsoft-color.svg +1 -0
- package/extensions/magpie/icons/mimocode.svg +1 -0
- package/extensions/magpie/icons/minimax-color.svg +1 -0
- package/extensions/magpie/icons/mistermorph-color.svg +1 -0
- package/extensions/magpie/icons/mistral-color.svg +1 -0
- package/extensions/magpie/icons/mlx-serve.png +0 -0
- package/extensions/magpie/icons/modelscope-color.svg +1 -0
- package/extensions/magpie/icons/monica-color.svg +1 -0
- package/extensions/magpie/icons/moonshot.svg +1 -0
- package/extensions/magpie/icons/morph-color.svg +1 -0
- package/extensions/magpie/icons/mothx.png +0 -0
- package/extensions/magpie/icons/nebius.svg +1 -0
- package/extensions/magpie/icons/newapi-color.svg +1 -0
- package/extensions/magpie/icons/nousresearch.svg +1 -0
- package/extensions/magpie/icons/nova-color.svg +1 -0
- package/extensions/magpie/icons/novita-color.svg +1 -0
- package/extensions/magpie/icons/nvidia-color.svg +1 -0
- package/extensions/magpie/icons/ollama.svg +1 -0
- package/extensions/magpie/icons/omlx.svg +1 -0
- package/extensions/magpie/icons/omo.svg +1 -0
- package/extensions/magpie/icons/omp.svg +1 -0
- package/extensions/magpie/icons/openai.svg +1 -0
- package/extensions/magpie/icons/openchamber.svg +11 -0
- package/extensions/magpie/icons/openclaw.svg +1 -0
- package/extensions/magpie/icons/opencode.svg +1 -0
- package/extensions/magpie/icons/openmausbot.svg +1 -0
- package/extensions/magpie/icons/openrouter.svg +1 -0
- package/extensions/magpie/icons/openwebui.svg +1 -0
- package/extensions/magpie/icons/parasail.svg +1 -0
- package/extensions/magpie/icons/penguin-color.svg +1 -0
- package/extensions/magpie/icons/perplexity-color.svg +1 -0
- package/extensions/magpie/icons/phala-color.svg +1 -0
- package/extensions/magpie/icons/pi.svg +1 -0
- package/extensions/magpie/icons/pipellm-color.svg +5 -0
- package/extensions/magpie/icons/poe.svg +1 -0
- package/extensions/magpie/icons/ppio-color.svg +1 -0
- package/extensions/magpie/icons/prime-agent.svg +1 -0
- package/extensions/magpie/icons/qiniu-color.svg +1 -0
- package/extensions/magpie/icons/qoder.svg +1 -0
- package/extensions/magpie/icons/qwen-color.svg +1 -0
- package/extensions/magpie/icons/qwenpaw.svg +1 -0
- package/extensions/magpie/icons/raycast.svg +1 -0
- package/extensions/magpie/icons/reasonix-color.svg +48 -0
- package/extensions/magpie/icons/reka.svg +1 -0
- package/extensions/magpie/icons/replicate.svg +1 -0
- package/extensions/magpie/icons/roocode.svg +1 -0
- package/extensions/magpie/icons/rtk.png +0 -0
- package/extensions/magpie/icons/sambanova-color.svg +1 -0
- package/extensions/magpie/icons/sensenova-color.svg +1 -0
- package/extensions/magpie/icons/siliconcloud-color.svg +1 -0
- package/extensions/magpie/icons/snow.png +0 -0
- package/extensions/magpie/icons/snowflake-color.svg +1 -0
- package/extensions/magpie/icons/sophnet-color.svg +1 -0
- package/extensions/magpie/icons/spark-color.svg +1 -0
- package/extensions/magpie/icons/stepfun-color.svg +1 -0
- package/extensions/magpie/icons/straico-color.svg +1 -0
- package/extensions/magpie/icons/submodel-color.svg +1 -0
- package/extensions/magpie/icons/t3code.svg +4 -0
- package/extensions/magpie/icons/targon-color.svg +1 -0
- package/extensions/magpie/icons/tencent-color.svg +1 -0
- package/extensions/magpie/icons/tencentcloud-color.svg +1 -0
- package/extensions/magpie/icons/tiangong-color.svg +1 -0
- package/extensions/magpie/icons/together-color.svg +1 -0
- package/extensions/magpie/icons/trae.svg +1 -0
- package/extensions/magpie/icons/typesafe.png +0 -0
- package/extensions/magpie/icons/upstage-color.svg +1 -0
- package/extensions/magpie/icons/venice-color.svg +1 -0
- package/extensions/magpie/icons/vercel.svg +1 -0
- package/extensions/magpie/icons/vertexai-color.svg +1 -0
- package/extensions/magpie/icons/vllm-color.svg +1 -0
- package/extensions/magpie/icons/volcengine-color.svg +1 -0
- package/extensions/magpie/icons/voyage-color.svg +1 -0
- package/extensions/magpie/icons/vscode.svg +1 -0
- package/extensions/magpie/icons/wenxin-color.svg +1 -0
- package/extensions/magpie/icons/windsurf.svg +1 -0
- package/extensions/magpie/icons/workbuddy-color.svg +1 -0
- package/extensions/magpie/icons/workersai-color.svg +1 -0
- package/extensions/magpie/icons/xai.svg +1 -0
- package/extensions/magpie/icons/xiaomimimo.svg +1 -0
- package/extensions/magpie/icons/xinference-color.svg +1 -0
- package/extensions/magpie/icons/yi.svg +1 -0
- package/extensions/magpie/icons/yuanbao-color.svg +1 -0
- package/extensions/magpie/icons/yylx.svg +5 -0
- package/extensions/magpie/icons/zai.svg +1 -0
- package/extensions/magpie/icons/zcode.png +0 -0
- package/extensions/magpie/icons/zed.svg +1 -0
- package/extensions/magpie/icons/zenmux.svg +1 -0
- package/extensions/magpie/icons/zeroone.svg +1 -0
- package/extensions/magpie/icons/zhipu-color.svg +1 -0
- package/extensions/magpie/icons/zoo-code.png +0 -0
- package/extensions/magpie/index.html +572 -0
- package/extensions/magpie/kernel.js +274 -0
- package/extensions/magpie/keyspend.js +63 -0
- package/extensions/magpie/layout.js +26 -0
- package/extensions/magpie/manifest.json +37 -0
- package/extensions/magpie/navlamp.js +47 -0
- package/extensions/magpie/omarchy.css +72 -0
- package/extensions/magpie/omarchy.js +46 -0
- package/extensions/magpie/passerrors.js +21 -0
- package/extensions/magpie/pluginpages.js +405 -0
- package/extensions/magpie/plugins/gateway/client.js +1194 -0
- package/extensions/magpie/plugins/gateway/keys.js +972 -0
- package/extensions/magpie/plugins/gateway/settings.js +459 -0
- package/extensions/magpie/plugins/library/client.js +6788 -0
- package/extensions/magpie/plugins/library/i18n.js +3228 -0
- package/extensions/magpie/plugins/library/library.css +848 -0
- package/extensions/magpie/plugins/otel/i18n.js +48 -0
- package/extensions/magpie/plugins/otel/page.js +189 -0
- package/extensions/magpie/plugins/redact/client.js +46 -0
- package/extensions/magpie/plugins/redact/demo.js +461 -0
- package/extensions/magpie/plugins/redact/i18n.js +432 -0
- package/extensions/magpie/plugins/redact/page.js +985 -0
- package/extensions/magpie/plugins/redact/redact.css +385 -0
- package/extensions/magpie/plugins/redact/settings.js +248 -0
- package/extensions/magpie/plugins/redact/xq_dark.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_dark.webm +0 -0
- package/extensions/magpie/plugins/redact/xq_light.jpg +0 -0
- package/extensions/magpie/plugins/redact/xq_light.webm +0 -0
- package/extensions/magpie/plugins/routing/client.js +8593 -0
- package/extensions/magpie/plugins/routing/i18n.js +2696 -0
- package/extensions/magpie/plugins/routing/routing.css +1111 -0
- package/extensions/magpie/plugins/search/settings.js +460 -0
- package/extensions/magpie/plugins/sessions/client.js +1622 -0
- package/extensions/magpie/plugins/sessions/i18n.js +360 -0
- package/extensions/magpie/plugins/sessions/sessions.css +181 -0
- package/extensions/magpie/plugins/sessions/settings.js +100 -0
- package/extensions/magpie/plugins/usage/client.js +150 -0
- package/extensions/magpie/plugins/usage/i18n.js +984 -0
- package/extensions/magpie/plugins/usage/page.js +4307 -0
- package/extensions/magpie/plugins.css +319 -0
- package/extensions/magpie/plugins.js +1926 -0
- package/extensions/magpie/profilerename.js +27 -0
- package/extensions/magpie/purposes.js +2 -0
- package/extensions/magpie/quitask.js +79 -0
- package/extensions/magpie/quotaall.js +142 -0
- package/extensions/magpie/ratewindows.js +123 -0
- package/extensions/magpie/registry.js +87 -0
- package/extensions/magpie/resetask.js +137 -0
- package/extensions/magpie/ruleedit.js +53 -0
- package/extensions/magpie/scrollends.js +91 -0
- package/extensions/magpie/settingshints.js +75 -0
- package/extensions/magpie/signsheets.js +86 -0
- package/extensions/magpie/start.js +26 -0
- package/extensions/magpie/syncfiles.js +216 -0
- package/extensions/magpie/theme.js +29 -0
- package/extensions/magpie/traywindows.js +41 -0
- package/extensions/magpie/webfonts.js +130 -0
- package/extensions/magpie/whatsnew.js +252 -0
- package/extensions/magpie.zip +0 -0
- package/knowledge/apps/baijiahao.md +30 -0
- package/knowledge/apps/bilibili.md +1 -0
- package/knowledge/apps/dedao.md +33 -0
- package/knowledge/apps/feishu.md +35 -0
- package/knowledge/apps/gmail.md +33 -0
- package/knowledge/apps/jike.md +31 -0
- package/knowledge/apps/kuaishou.md +34 -0
- package/knowledge/apps/linkedin.md +33 -0
- package/knowledge/apps/qqmail.md +30 -0
- package/knowledge/apps/telegram.md +27 -0
- package/knowledge/apps/toutiao.md +32 -0
- package/knowledge/apps/uber-eats.md +26 -0
- package/knowledge/apps/uber.md +27 -0
- package/knowledge/apps/whatsapp.md +27 -0
- package/knowledge/apps/x.md +32 -0
- package/knowledge/apps/xiaohongshu.md +1 -0
- package/knowledge/apps/zhihu.md +1 -0
- package/knowledge/sites/bilibili.com/SITE.md +50 -0
- package/knowledge/sites/bilibili.com/references/view-fields.md +17 -0
- package/knowledge/sites/github.com/SITE.md +51 -0
- package/knowledge/sites/github.com/references/repo-fields.md +13 -0
- package/knowledge/sites/x.com/SITE.md +47 -0
- package/knowledge/sites/xiaohongshu.com/SITE.md +48 -0
- package/knowledge/sites/xiaohongshu.com/references/note-fields.md +18 -0
- package/knowledge/sites/youtube.com/SITE.md +49 -0
- package/knowledge/sites/youtube.com/references/innertube-fields.md +14 -0
- package/knowledge/sites/zhihu.com/SITE.md +47 -0
- package/knowledge/sites/zhihu.com/references/api-fields.md +21 -0
- package/lib/mobile/extension-catalog.js +41 -1
- package/lib/mobile/extension-data.js +224 -0
- package/lib/mobile/extension-dev.js +3 -1
- package/lib/mobile/file-ops.js +252 -0
- package/lib/mobile/history-derive.js +80 -4
- package/lib/mobile/host-bridge/endpoints.js +48 -6
- package/lib/mobile/host-bridge/event-synthesis.js +33 -6
- package/lib/mobile/jobs-control.js +118 -0
- package/lib/mobile/memory.js +635 -0
- package/lib/mobile/model-badges.js +77 -0
- package/lib/mobile/pet-tools.js +50 -4
- package/lib/mobile/pets.js +214 -18
- package/lib/mobile/phone/access.js +85 -0
- package/lib/mobile/phone/evidence.js +228 -0
- package/lib/mobile/phone/gate.js +290 -0
- package/lib/mobile/phone/grounding.js +314 -0
- package/lib/mobile/phone/guard.js +465 -0
- package/lib/mobile/phone/irreversible.js +88 -8
- package/lib/mobile/phone/knowledge-tools.js +665 -0
- package/lib/mobile/phone/learn.js +329 -0
- package/lib/mobile/phone/redact.js +237 -0
- package/lib/mobile/phone/risk.js +95 -0
- package/lib/mobile/phone/routes.js +28 -1
- package/lib/mobile/phone/service.js +23 -0
- package/lib/mobile/phone/site-commands.js +271 -0
- package/lib/mobile/phone/step-summary.js +201 -0
- package/lib/mobile/phone/stop-summary.js +235 -0
- package/lib/mobile/phone/tool-groups.js +125 -0
- package/lib/mobile/phone/tools.js +589 -0
- package/lib/mobile/phone/untrusted.js +65 -0
- package/lib/mobile/phone-knowledge.js +20 -4
- package/lib/mobile/schedule-edit.js +500 -0
- package/lib/mobile/schedule.js +5 -0
- package/lib/mobile/send-continuation.js +52 -0
- package/lib/mobile/session-models.js +27 -31
- package/lib/mobile/session-send.js +12 -12
- package/lib/mobile/skill-import.js +338 -0
- package/lib/mobile/skill-store.js +524 -0
- package/lib/routes/mobile-api.js +42 -0
- package/package.json +7 -6
- package/skills/skill-authoring/SKILL.md +46 -0
- package/skills/skill-authoring/references/examples.md +82 -0
- package/skills/skill-authoring/references/frontmatter-and-fields.md +46 -0
- package/skills/skill-authoring/scripts/check_skill.py +167 -0
- package/skills/skill-authoring/scripts/manifest.json +3 -0
- package/knowledge/sites/bilibili.com.md +0 -27
- package/knowledge/sites/github.com.md +0 -27
- package/knowledge/sites/x.com.md +0 -28
- package/knowledge/sites/xiaohongshu.com.md +0 -27
- package/knowledge/sites/youtube.com.md +0 -28
- package/knowledge/sites/zhihu.com.md +0 -27
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
// Insights › Masking's code (client.js has its markup and hooks): run after
|
|
2
|
+
// app.js, whose masking helpers it shares (maskTip, maskColor,
|
|
3
|
+
// maskShape, maskValue: Settings' Try it and the masked calls' badges use
|
|
4
|
+
// them too; Try it uses this page's maskMatches in turn). What a
|
|
5
|
+
// placeholder stood for is in this magpie's memory alone, never on disk,
|
|
6
|
+
// so after a restart it is gone; so is the text a request went out with
|
|
7
|
+
// (/api/masking/sent), kept for the newest requests only.
|
|
8
|
+
|
|
9
|
+
const stillMotion = calm;
|
|
10
|
+
// maskMatches is text's stand-ins as matchAll gives them: its {{…}}
|
|
11
|
+
// placeholders, and the made-up keys "Keep a key's form" sent (redact's
|
|
12
|
+
// shape.go), the keys of info that are no placeholder, which only the
|
|
13
|
+
// record's own list tells from the text around them (#3599)
|
|
14
|
+
const maskLookRes = new WeakMap();
|
|
15
|
+
function maskMatches(text, info) {
|
|
16
|
+
let re = maskLookRes.get(info);
|
|
17
|
+
if (!re) {
|
|
18
|
+
const looks = [...info.keys()].filter((p) => p && !p.startsWith("{{")).sort((a, b) => b.length - a.length)
|
|
19
|
+
.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
20
|
+
re = looks.length ? new RegExp(PLACEHOLDER.source + "|" + looks.join("|"), "g") : PLACEHOLDER;
|
|
21
|
+
maskLookRes.set(info, re);
|
|
22
|
+
}
|
|
23
|
+
return text.matchAll(re);
|
|
24
|
+
}
|
|
25
|
+
// the providers' names, read once when the page opened before any page
|
|
26
|
+
// that loads them
|
|
27
|
+
let maskProv = null, maskProvAsked = false;
|
|
28
|
+
function maskWho(id, kind) {
|
|
29
|
+
const x = kind === "agent" ? state.agents?.find((a) => a.id === id) : (providers || maskProv)?.providers?.find((p) => p.id === id);
|
|
30
|
+
return { name: x?.name || id || t("Unknown"), icon: x?.icon };
|
|
31
|
+
}
|
|
32
|
+
// what a request was, beside a chat (gateway's noteMasked)
|
|
33
|
+
const MASK_WHAT = { search: "Web search", image: "Image", video: "Video", decide: "System One", embeddings: "Embeddings", audio: "Audio" };
|
|
34
|
+
// maskDayName: Today, Yesterday or the date a masked request went on
|
|
35
|
+
function maskDayName(when) {
|
|
36
|
+
const d = new Date(when), now = new Date();
|
|
37
|
+
const days = Math.round((new Date(now.toDateString()) - new Date(d.toDateString())) / 86400000);
|
|
38
|
+
if (days === 0) return t("Today");
|
|
39
|
+
if (days === 1) return t("Yesterday");
|
|
40
|
+
return d.toLocaleDateString(intlLang(), { weekday: "short", month: "short", day: "numeric" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- the summary: over 7 or 30 days, kept per machine ----
|
|
44
|
+
let maskSumDays = 0, maskAt = 0, maskBusy = null, maskBusyDays = 0;
|
|
45
|
+
let maskDays = 7;
|
|
46
|
+
try { if (localStorage.getItem("magpie.maskDays") === "30") maskDays = 30; } catch {}
|
|
47
|
+
let maskDrawn = ""; // what the card says, so a redraw that changes nothing is skipped
|
|
48
|
+
let maskKeyRead = false; // the address's key=, a masked call opened from the tray panel
|
|
49
|
+
// the list: by request or by value, and what it is narrowed to
|
|
50
|
+
const maskL = { mode: "req", facet: "", kind: "", agent: "", provider: "", records: [], more: false, busy: false, error: "", read: false };
|
|
51
|
+
let maskO = null; // the request open, while it is
|
|
52
|
+
|
|
53
|
+
// maskLoad reads the summary for the days shown, when what is kept is
|
|
54
|
+
// older than 10s or for other days; forced, it reads again whatever is kept
|
|
55
|
+
function maskLoad(force) {
|
|
56
|
+
const days = maskDays;
|
|
57
|
+
if (maskBusy && maskBusyDays === days) return maskBusy;
|
|
58
|
+
if (!force && maskSum && maskSumDays === days && Date.now() - maskAt < 10000) return null;
|
|
59
|
+
maskBusyDays = days;
|
|
60
|
+
const p = maskBusy = api("masking/summary?days=" + days).then((sum) => {
|
|
61
|
+
if (days !== maskDays) return;
|
|
62
|
+
maskSum = { days: [], kinds: [], agents: [], providers: [], values: [], by: {}, n: 0, distinct: 0, requests: 0, ...(sum || {}) };
|
|
63
|
+
maskSumDays = days;
|
|
64
|
+
maskAt = Date.now();
|
|
65
|
+
}).catch(() => {}).finally(() => { if (maskBusy === p) maskBusy = null; });
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
const maskSettings = () => prefs || state?.settings || {};
|
|
69
|
+
// on as the masker is (masking.Wanted): your own words mask with neither
|
|
70
|
+
// switch on
|
|
71
|
+
const maskOn = () => { const s = maskSettings(); return !!(s.redact || s.redactPersonal || s.redactWords?.length); };
|
|
72
|
+
|
|
73
|
+
function maskPageOpen() {
|
|
74
|
+
if (!providers && !maskProvAsked) {
|
|
75
|
+
maskProvAsked = true;
|
|
76
|
+
api("providers").then((r) => { maskProv = r; maskDrawn = ""; maskDrawAll(); }).catch(() => { maskProvAsked = false; });
|
|
77
|
+
}
|
|
78
|
+
// a masked call in the tray panel opens the window on its request
|
|
79
|
+
if (!maskKeyRead) {
|
|
80
|
+
maskKeyRead = true;
|
|
81
|
+
const k = params.get("key");
|
|
82
|
+
if (k) {
|
|
83
|
+
const q = new URLSearchParams(location.search);
|
|
84
|
+
q.delete("key");
|
|
85
|
+
history.replaceState(null, "", "?" + q);
|
|
86
|
+
maskOneOpen(k);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return maskPageLoad();
|
|
90
|
+
}
|
|
91
|
+
function maskPageLoad(force) {
|
|
92
|
+
maskDrawAll();
|
|
93
|
+
(prefs ? Promise.resolve() : loadSettings()).then(drawMaskAlert).catch(() => {});
|
|
94
|
+
const sum = maskLoad(force)?.then(() => { drawMaskCard(); drawMaskList(); });
|
|
95
|
+
const recs = maskL.read && force ? maskRefresh() : maskL.read ? null : maskFetch();
|
|
96
|
+
return Promise.all([sum, recs]);
|
|
97
|
+
}
|
|
98
|
+
function maskDrawAll() {
|
|
99
|
+
drawMaskCard();
|
|
100
|
+
drawMaskList();
|
|
101
|
+
if (maskO) drawMaskOne();
|
|
102
|
+
}
|
|
103
|
+
// the Masking alert (Must2333 on X): a notification when a value is
|
|
104
|
+
// masked for the first time, off until turned on. Gateway mode leaves it
|
|
105
|
+
// out, as it does the other alerts its desktop would show
|
|
106
|
+
function drawMaskAlert() {
|
|
107
|
+
const s = prefs || {};
|
|
108
|
+
$("#maskPrefs").hidden = gatewayMode;
|
|
109
|
+
$("#maskAlertSegs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.maskAlert ? "on" : "off",
|
|
110
|
+
(v) => savePrefs({ ...prefsKeep(s), maskAlert: v === "on" }).then(drawMaskAlert)));
|
|
111
|
+
}
|
|
112
|
+
// while the page is on screen it keeps up with requests as they go
|
|
113
|
+
setInterval(() => {
|
|
114
|
+
if (view !== "masking" || document.hidden) return;
|
|
115
|
+
maskLoad(true)?.then(() => { drawMaskCard(); if (maskL.mode === "val" || !maskO) drawMaskList(); });
|
|
116
|
+
if (!maskL.busy) maskRefresh();
|
|
117
|
+
}, 15000);
|
|
118
|
+
|
|
119
|
+
// ---- the card: how much was kept, and where it went ----
|
|
120
|
+
function drawMaskCard() {
|
|
121
|
+
const box = $("#maskCard");
|
|
122
|
+
if (!box) return;
|
|
123
|
+
// until the window's state is read, agents would show as their ids
|
|
124
|
+
const on = maskOn(), sum = maskSumDays === maskDays && load.done ? maskSum : null;
|
|
125
|
+
const key = JSON.stringify([document.documentElement.lang, maskDays, on, !!maskSettings().redactWords?.length, sum, state?.agents?.length, (providers || maskProv)?.providers?.length]);
|
|
126
|
+
if (key === maskDrawn && box.firstChild) return;
|
|
127
|
+
maskDrawn = key;
|
|
128
|
+
// it moves when it first has numbers to say, and for other days: not
|
|
129
|
+
// on every poll
|
|
130
|
+
const move = !!sum && box.dataset.drawn !== String(maskDays) && !stillMotion();
|
|
131
|
+
if (sum) box.dataset.drawn = String(maskDays);
|
|
132
|
+
maskTipOff();
|
|
133
|
+
box.replaceChildren();
|
|
134
|
+
box.classList.remove("off");
|
|
135
|
+
if (sum && !sum.requests && !on) return maskOffCard(box);
|
|
136
|
+
|
|
137
|
+
const head = el("div", "msk-head"), words = el("div", "msk-title");
|
|
138
|
+
words.append(el("h2", "", t("Kept from vendors")),
|
|
139
|
+
el("span", "msk-sub", on ? t("Masked on this computer before each request left it, and put back in each answer") : t("Masking is off: requests go to vendors as they are")));
|
|
140
|
+
head.append(words, el("span", "grow"));
|
|
141
|
+
if (!on) {
|
|
142
|
+
const b = el("button", "text", t("Turn on masking"));
|
|
143
|
+
b.type = "button";
|
|
144
|
+
b.onclick = maskTurnOn;
|
|
145
|
+
head.append(b);
|
|
146
|
+
} else head.append(maskOffButton());
|
|
147
|
+
if (sum?.requests || maskDays === 30) head.append(segs([["7", t("7 days")], ["30", t("30 days")]], String(maskDays), (v) => {
|
|
148
|
+
maskDays = +v;
|
|
149
|
+
try { localStorage.setItem("magpie.maskDays", v); } catch {}
|
|
150
|
+
maskLoad(true)?.then(() => { drawMaskCard(); drawMaskList(); });
|
|
151
|
+
}));
|
|
152
|
+
box.append(head);
|
|
153
|
+
const band = el("div", "msk-band");
|
|
154
|
+
box.append(band);
|
|
155
|
+
if (!sum) {
|
|
156
|
+
band.append(el("span", "skeleton msk-sk-num"), el("span", "skeleton msk-sk-flow"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// the number, what makes it up, and the days
|
|
161
|
+
const nums = el("div", "msk-nums");
|
|
162
|
+
const num = el("div", "msk-num", sum.n.toLocaleString(intlLang()));
|
|
163
|
+
nums.append(num, el("div", "msk-unit", t(sum.n === 1 ? "value masked" : "values masked")));
|
|
164
|
+
const stats = el("div", "msk-stats");
|
|
165
|
+
const stat = (label, n, cls) => { const s = el("span", cls || ""); s.append(el("b", "", fmtN(n)), document.createTextNode(label)); stats.append(s); };
|
|
166
|
+
stat(t("Different values"), sum.distinct);
|
|
167
|
+
stat(t("Requests"), sum.requests);
|
|
168
|
+
if (sum.blocked) stat(t("Held back"), sum.blocked, "held");
|
|
169
|
+
nums.append(stats);
|
|
170
|
+
const days = el("div", "msk-days");
|
|
171
|
+
nums.append(days);
|
|
172
|
+
band.append(nums);
|
|
173
|
+
maskStrip(days, sum.days, move);
|
|
174
|
+
if (move && sum.n) maskCountUp(num, sum.n);
|
|
175
|
+
if (sum.agents.length && sum.providers.length) band.append(maskFlow(sum, move));
|
|
176
|
+
else band.append(maskNone());
|
|
177
|
+
if (move) for (const [i, n] of [...band.children].entries()) n.animate([{ opacity: 0, transform: "translateY(6px)", filter: "blur(6px)" }, {}], { duration: 900, delay: i * 120, easing: "cubic-bezier(.2, .7, .1, 1)", fill: "backwards" });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// masking off and nothing ever masked: what it would do, and the switch
|
|
181
|
+
function maskOffCard(box) {
|
|
182
|
+
box.classList.add("off");
|
|
183
|
+
const s = svg(SHIELD, 40, .9);
|
|
184
|
+
s.classList.add("msk-off-ic");
|
|
185
|
+
const b = el("button", "text primary", t("Turn on masking"));
|
|
186
|
+
b.type = "button";
|
|
187
|
+
b.onclick = maskTurnOn;
|
|
188
|
+
const also = el("span", "msk-also");
|
|
189
|
+
const link = el("button", "text", t("Settings › Privacy"));
|
|
190
|
+
link.type = "button";
|
|
191
|
+
link.onclick = (e) => openSettingsAt(e, "privacy");
|
|
192
|
+
also.append(el("span", "", t("You can also change it in")), link);
|
|
193
|
+
box.append(s, el("h2", "", t("Requests go to vendors as they are")),
|
|
194
|
+
el("p", "", t("Turned on, keys, passwords and tokens become placeholders before they leave this computer, and are put back in each answer. Emails and phone numbers too once Mask personal data is on. What each request had masked shows here.")),
|
|
195
|
+
b, also);
|
|
196
|
+
}
|
|
197
|
+
// maskNone: what the card says while nothing was masked. With secrets
|
|
198
|
+
// alone on, an agent's everyday requests carry nothing to mask (an email
|
|
199
|
+
// or a name isn't a secret), so it says that, and where personal data is
|
|
200
|
+
// turned on, rather than a 0 that reads as masking not working
|
|
201
|
+
function maskNone() {
|
|
202
|
+
const s = maskSettings(), box = el("div", "msk-flow-none");
|
|
203
|
+
if (s.redactPersonal || !s.redact) {
|
|
204
|
+
box.textContent = t("A request that carries a key, a password or personal data shows here as it goes");
|
|
205
|
+
return box;
|
|
206
|
+
}
|
|
207
|
+
box.append(el("span", "", t("Only keys, passwords and tokens are masked now, and a request that carries one shows here as it goes. Emails and phone numbers are masked once Mask personal data is on, in")));
|
|
208
|
+
const link = el("button", "text", t("Settings › Privacy"));
|
|
209
|
+
link.type = "button";
|
|
210
|
+
link.onclick = (e) => openSettingsAt(e, "privacy");
|
|
211
|
+
box.append(" ", link);
|
|
212
|
+
return box;
|
|
213
|
+
}
|
|
214
|
+
// the way off, beside the way on (ZekeXiao on X: "脱敏好像只有一个打开
|
|
215
|
+
// 开关,不能关?"). Both switches go off at once. Your own words mask with neither
|
|
216
|
+
// on, and only Settings › Privacy can take them out, so with words the
|
|
217
|
+
// button opens it there, and says so, rather than leave the page
|
|
218
|
+
// saying off while words still mask
|
|
219
|
+
function maskOffButton() {
|
|
220
|
+
const words = maskSettings().redactWords?.length;
|
|
221
|
+
const b = el("button", "text", t(words ? "Turn off in Settings" : "Turn off masking"));
|
|
222
|
+
b.type = "button";
|
|
223
|
+
if (words) {
|
|
224
|
+
b.title = t("Your own masked words still mask until you take them out, in Settings › Privacy");
|
|
225
|
+
b.onclick = (e) => openSettingsAt(e, "privacy");
|
|
226
|
+
return b;
|
|
227
|
+
}
|
|
228
|
+
b.onclick = () => {
|
|
229
|
+
b.disabled = true;
|
|
230
|
+
(prefs ? Promise.resolve() : loadSettings())
|
|
231
|
+
.then(() => {
|
|
232
|
+
// turned on again from this page, it comes back as it was
|
|
233
|
+
maskWasPersonal = !!prefs.redactPersonal;
|
|
234
|
+
return savePrefs({ ...prefsKeep(prefs), redact: false, redactPersonal: false });
|
|
235
|
+
})
|
|
236
|
+
.then(() => { maskDrawn = ""; drawMaskCard(); })
|
|
237
|
+
.finally(() => { b.disabled = false; });
|
|
238
|
+
};
|
|
239
|
+
return b;
|
|
240
|
+
}
|
|
241
|
+
let maskWasPersonal = false;
|
|
242
|
+
function maskTurnOn(e) {
|
|
243
|
+
const b = e.currentTarget;
|
|
244
|
+
b.disabled = true;
|
|
245
|
+
(prefs ? Promise.resolve() : loadSettings())
|
|
246
|
+
.then(() => {
|
|
247
|
+
const back = maskWasPersonal;
|
|
248
|
+
maskWasPersonal = false;
|
|
249
|
+
if (back) window.hideAccounts?.set(true, true);
|
|
250
|
+
return savePrefs({ ...prefsKeep(prefs), redact: true, ...(back ? { redactPersonal: true } : {}) });
|
|
251
|
+
})
|
|
252
|
+
.then(() => { maskDrawn = ""; drawMaskCard(); })
|
|
253
|
+
.finally(() => { b.disabled = false; });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function maskCountUp(node, n) {
|
|
257
|
+
const start = performance.now(), ms = 1100;
|
|
258
|
+
const step = (now) => {
|
|
259
|
+
if (!node.isConnected) return;
|
|
260
|
+
const p = Math.min(1, (now - start) / ms);
|
|
261
|
+
const e = p === 1 ? 1 : 1 - Math.pow(2, -10 * p);
|
|
262
|
+
node.textContent = Math.round(n * e).toLocaleString(intlLang());
|
|
263
|
+
if (p < 1) requestAnimationFrame(step);
|
|
264
|
+
};
|
|
265
|
+
requestAnimationFrame(step);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// maskStrip: the values masked each day, a bar a day on a hairline, today
|
|
269
|
+
// in the accent; the day pointed at said over it
|
|
270
|
+
function maskStrip(box, days, move) {
|
|
271
|
+
const total = days.reduce((a, d) => a + d.n, 0);
|
|
272
|
+
box.setAttribute("role", "img");
|
|
273
|
+
box.setAttribute("aria-label", t("{n} values masked over {days} days", { n: fmtN(total), days: days.length }));
|
|
274
|
+
if (!days.length) return;
|
|
275
|
+
const max = Math.max(1, ...days.map((d) => d.n));
|
|
276
|
+
const bars = el("div", "msk-bars" + (days.length > 14 ? " many" : ""));
|
|
277
|
+
const tip = el("div", "msk-ctip");
|
|
278
|
+
tip.hidden = true;
|
|
279
|
+
const fmt = (d) => d === days.at(-1).day ? t("Today") : new Date(d + "T00:00").toLocaleDateString(intlLang(), { month: "short", day: "numeric" });
|
|
280
|
+
days.forEach((d, i) => {
|
|
281
|
+
const b = el("span", "msk-day" + (i === days.length - 1 ? " now" : "") + (d.n ? "" : " zero"));
|
|
282
|
+
const f = el("i");
|
|
283
|
+
if (d.n) f.style.height = Math.max(8, (100 * d.n) / max) + "%";
|
|
284
|
+
b.append(f);
|
|
285
|
+
b.onpointerenter = () => {
|
|
286
|
+
tip.hidden = false;
|
|
287
|
+
tip.textContent = `${fmt(d.day)} · ${t(d.n === 1 ? "1 value masked" : "{n} values masked", { n: fmtN(d.n) })} · ${t(d.requests === 1 ? "1 request" : "{n} requests", { n: fmtN(d.requests) })}`;
|
|
288
|
+
const w = tip.offsetWidth, W = box.clientWidth, x = b.offsetLeft + b.offsetWidth / 2;
|
|
289
|
+
tip.style.left = Math.max(w / 2, Math.min(x, W - w / 2)) + "px";
|
|
290
|
+
};
|
|
291
|
+
b.onpointerleave = () => { tip.hidden = true; };
|
|
292
|
+
if (move && d.n) f.animate([{ transform: "scaleY(0)" }, { transform: "none" }], { duration: 760, delay: 160 + (i * 520) / days.length, easing: "cubic-bezier(.32, .72, 0, 1)", fill: "backwards" });
|
|
293
|
+
bars.append(b);
|
|
294
|
+
});
|
|
295
|
+
const axis = el("div", "msk-axis");
|
|
296
|
+
axis.append(el("span", "", fmt(days[0].day)), el("span", "", fmt(days.at(-1).day)));
|
|
297
|
+
box.append(bars, axis, tip);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// maskFlow: hairlines from the agents into this computer, uneven as
|
|
301
|
+
// written, and out of it to the providers, even as placeholders; magpie's
|
|
302
|
+
// bird on the line between, so a screenshot says who masked them
|
|
303
|
+
function maskFlow(sum, move) {
|
|
304
|
+
const sec = el("div", "msk-fl"), flow = el("div", "mo-flow");
|
|
305
|
+
const side = (list, kind) => {
|
|
306
|
+
const shown = list.slice(0, 3), rest = list.slice(3);
|
|
307
|
+
const rows = shown.map((x) => ({ id: x.name, n: x.n, ...maskWho(x.name, kind) }));
|
|
308
|
+
if (rest.length) rows.push({ id: "", n: rest.reduce((a, x) => a + x.n, 0), name: t("{n} more", { n: rest.length }) });
|
|
309
|
+
const col = el("div", "mo-col " + kind);
|
|
310
|
+
for (const r of rows) {
|
|
311
|
+
const b = el("button", "mo-node" + (r.id && maskL[kind] === r.id ? " on" : ""));
|
|
312
|
+
b.type = "button";
|
|
313
|
+
// its logo first, then its name, on both sides; "N more" keeps the
|
|
314
|
+
// logo's room so the names line up
|
|
315
|
+
const ic = r.id ? icon(r.icon || "generic") : el("span", "ic");
|
|
316
|
+
const tx = el("span", "tx");
|
|
317
|
+
tx.append(el("span", "nm", r.name), el("span", "ct", fmtN(r.n)));
|
|
318
|
+
b.append(ic, tx);
|
|
319
|
+
if (r.id) {
|
|
320
|
+
b.title = t("Show what {name} sent masked", { name: r.name });
|
|
321
|
+
b.onclick = () => maskNarrow({ [kind]: maskL[kind] === r.id ? "" : r.id });
|
|
322
|
+
} else b.disabled = true;
|
|
323
|
+
col.append(b);
|
|
324
|
+
}
|
|
325
|
+
return [col, rows];
|
|
326
|
+
};
|
|
327
|
+
const [from, fromRows] = side(sum.agents, "agent");
|
|
328
|
+
const [to, toRows] = side(sum.providers, "provider");
|
|
329
|
+
const mid = el("div", "mo-mid");
|
|
330
|
+
const rowH = 34, H = Math.max(fromRows.length, toRows.length, 3) * rowH;
|
|
331
|
+
const max = Math.max(...fromRows.map((r) => r.n), ...toRows.map((r) => r.n), 1);
|
|
332
|
+
const hub = el("span", "msk-hub");
|
|
333
|
+
hub.title = "magpie";
|
|
334
|
+
hub.innerHTML = '<svg viewBox="0 0 44 44" aria-hidden="true"><use href="#bird"/></svg>';
|
|
335
|
+
// drawn in the middle's own pixels on each resize: a stretched viewBox
|
|
336
|
+
// would bend the hairlines
|
|
337
|
+
const s = sv("svg", { "aria-hidden": "true" });
|
|
338
|
+
let drawnW = 0;
|
|
339
|
+
const draw = () => {
|
|
340
|
+
const W = Math.round(mid.clientWidth);
|
|
341
|
+
if (!W || W === drawnW) return;
|
|
342
|
+
drawnW = W;
|
|
343
|
+
s.setAttribute("viewBox", `0 0 ${W} ${H}`);
|
|
344
|
+
s.replaceChildren();
|
|
345
|
+
const m = W / 2, still = stillMotion(), lines = [];
|
|
346
|
+
const add = (tag, a, to = s) => { const n = sv(tag, a); to.append(n); return n; };
|
|
347
|
+
const curve = (x0, y0, x1, y1) => `M${x0},${y0}C${(x0 + x1) / 2},${y0} ${(x0 + x1) / 2},${y1} ${x1},${y1}`;
|
|
348
|
+
const bundle = (rows, out) => {
|
|
349
|
+
const ks = rows.map((r) => Math.max(1, Math.round(18 * r.n / max))), all = ks.reduce((a, k) => a + k, 0);
|
|
350
|
+
let j = 0, rnd = out ? 11 : 7;
|
|
351
|
+
rows.forEach((r, i) => {
|
|
352
|
+
const y = (H - rows.length * rowH) / 2 + (i + .5) * rowH;
|
|
353
|
+
for (let q = 0; q < ks[i]; q++, j++) {
|
|
354
|
+
const at = all > 1 ? H * (.22 + .56 * j / (all - 1)) : H / 2;
|
|
355
|
+
rnd = (rnd * 16807) % 2147483647;
|
|
356
|
+
const end = y + (q - (ks[i] - 1) / 2) * 1.4 + (out ? 0 : (rnd % 30 - 15) / 10);
|
|
357
|
+
const d = out ? curve(m, at, W, end) : curve(0, end, m, at);
|
|
358
|
+
lines.push(add("path", { d, pathLength: 1, class: "mo-th" + (out ? " out" : "") + (ks[i] === 1 ? " one" : ""), "stroke-opacity": ks[i] === 1 ? (out ? .8 : .4) : out ? .42 : .07 + (rnd % 20) / 100 }));
|
|
359
|
+
// a value goes in round, a placeholder out square
|
|
360
|
+
if (q === ks[i] >> 1 && !still) for (let k = 0, dots = 1 + Math.round(2 * r.n / max); k < dots; k++) {
|
|
361
|
+
const dur = 1.7 + i * .5, begin = (out ? dur : 0) + k * 2 * dur / dots + "s";
|
|
362
|
+
const z = out ? 1.8 : 1.7, dot = add("rect", { x: -z, y: -z, width: 2 * z, height: 2 * z, rx: out ? .6 : z, class: "mo-dot" + (out ? " out" : "") });
|
|
363
|
+
// twice round when drawn, not forever: an endless motion lays the
|
|
364
|
+
// page out each frame, ~16% of a core while it sits open (#3185)
|
|
365
|
+
const a = { dur: dur + "s", begin, repeatCount: 2 };
|
|
366
|
+
add("animateMotion", { path: d, ...a }, dot);
|
|
367
|
+
add("animate", { attributeName: "opacity", values: "0;1;1;0", keyTimes: "0;.1;.85;1", ...a }, dot);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
bundle(fromRows, false);
|
|
373
|
+
bundle(toRows, true);
|
|
374
|
+
// this computer: a ticked line, and a mark going down it
|
|
375
|
+
add("line", { x1: m, y1: 0, x2: m, y2: H, class: "mo-mem" });
|
|
376
|
+
for (let y = 0; y <= H; y += 17) add("line", { x1: m - 3, y1: y, x2: m + 3, y2: y, class: "mo-tick" });
|
|
377
|
+
if (!still) add("rect", { x: m - 1, y: 0, width: 2, height: 18, rx: 1, class: "mo-scan" }).style.setProperty("--h", H - 18 + "px");
|
|
378
|
+
// drawn in once, the way in first
|
|
379
|
+
if (move) lines.forEach((p) => {
|
|
380
|
+
p.animate([{ strokeDashoffset: 1 }, {}], { duration: 2400, delay: p.classList.contains("out") ? 700 : 300, easing: "cubic-bezier(.3, .6, .1, 1)", fill: "backwards" });
|
|
381
|
+
});
|
|
382
|
+
move = false;
|
|
383
|
+
};
|
|
384
|
+
mid.append(s, hub);
|
|
385
|
+
flow.style.height = H + "px";
|
|
386
|
+
flow.append(from, mid, to);
|
|
387
|
+
new ResizeObserver(draw).observe(mid);
|
|
388
|
+
sec.append(flow, el("div", "mo-cap", t("magpie masked these on this computer")));
|
|
389
|
+
return sec;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ---- the list: each request masking changed, or each value ----
|
|
393
|
+
const MASK_FACETS = [
|
|
394
|
+
["", "All", (s) => s.n],
|
|
395
|
+
["secret", "Secrets", (s) => (s.kinds || []).filter((k) => MASK_SECRET_KINDS.includes(k.name)).reduce((a, k) => a + k.n, 0)],
|
|
396
|
+
["rule", "Rules", (s) => s.by?.rule || 0],
|
|
397
|
+
["model", "Local model", (s) => s.by?.model || 0],
|
|
398
|
+
["blocked", "Held back", (s) => s.blocked || 0],
|
|
399
|
+
];
|
|
400
|
+
function maskQuery(before) {
|
|
401
|
+
const q = new URLSearchParams({ limit: "40" });
|
|
402
|
+
if (before) q.set("before", before);
|
|
403
|
+
const f = maskL.facet;
|
|
404
|
+
if (maskL.kind) q.set("kind", maskL.kind);
|
|
405
|
+
else if (f === "secret") q.set("kind", MASK_SECRET_KINDS.join(","));
|
|
406
|
+
if (f === "rule" || f === "model") q.set("by", f);
|
|
407
|
+
if (f === "blocked") q.set("blocked", "1");
|
|
408
|
+
if (maskL.agent) q.set("agent", maskL.agent);
|
|
409
|
+
if (maskL.provider) q.set("provider", maskL.provider);
|
|
410
|
+
return "masking/records?" + q;
|
|
411
|
+
}
|
|
412
|
+
// maskNarrow: the list for other filters, read again from the start
|
|
413
|
+
function maskNarrow(change) {
|
|
414
|
+
Object.assign(maskL, change);
|
|
415
|
+
maskDrawn = "";
|
|
416
|
+
drawMaskCard();
|
|
417
|
+
return maskFetch();
|
|
418
|
+
}
|
|
419
|
+
let maskSeq = 0;
|
|
420
|
+
function maskFetch(more) {
|
|
421
|
+
const seq = ++maskSeq;
|
|
422
|
+
maskL.busy = true;
|
|
423
|
+
maskL.error = "";
|
|
424
|
+
drawMaskList();
|
|
425
|
+
return api(maskQuery(more ? maskL.records.at(-1)?.key : 0))
|
|
426
|
+
.then((r) => {
|
|
427
|
+
if (seq !== maskSeq) return;
|
|
428
|
+
const got = r?.records || [];
|
|
429
|
+
maskL.records = more ? [...maskL.records, ...got] : got;
|
|
430
|
+
maskL.more = !!r?.more;
|
|
431
|
+
})
|
|
432
|
+
.catch((e) => { if (seq === maskSeq) maskL.error = e.message; })
|
|
433
|
+
.finally(() => {
|
|
434
|
+
if (seq !== maskSeq) return;
|
|
435
|
+
maskL.busy = false;
|
|
436
|
+
maskL.read = true;
|
|
437
|
+
drawMaskList();
|
|
438
|
+
if (maskO) drawMaskOne(); // its previous and next
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// new records come in over the ones shown, which keep their place
|
|
442
|
+
function maskRefresh() {
|
|
443
|
+
if (!maskL.records.length) return maskFetch();
|
|
444
|
+
const seq = maskSeq;
|
|
445
|
+
return api(maskQuery(0)).then((r) => {
|
|
446
|
+
if (seq !== maskSeq || !maskL.records.length) return;
|
|
447
|
+
const fresh = (r?.records || []).filter((x) => BigInt(x.key) > BigInt(maskL.records[0].key));
|
|
448
|
+
if (!fresh.length) return;
|
|
449
|
+
maskL.records = [...fresh, ...maskL.records];
|
|
450
|
+
if (!maskO) drawMaskList();
|
|
451
|
+
else drawMaskOne();
|
|
452
|
+
}).catch(() => {});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function drawMaskList() {
|
|
456
|
+
const box = $("#maskList");
|
|
457
|
+
if (!box) return;
|
|
458
|
+
const sum = (maskSumDays === maskDays && maskSum) || null;
|
|
459
|
+
maskTipOff();
|
|
460
|
+
box.replaceChildren();
|
|
461
|
+
const bar = el("div", "msk-bar");
|
|
462
|
+
bar.append(segs([["req", t("By request")], ["val", t("By value")]], maskL.mode, (v) => { maskL.mode = v; drawMaskList(); }));
|
|
463
|
+
const live = el("span", "msk-live");
|
|
464
|
+
live.title = t("Read again every 15 seconds while this page is open");
|
|
465
|
+
live.append(el("i"), el("span", "", t("Live")));
|
|
466
|
+
bar.append(live, el("span", "grow"));
|
|
467
|
+
if (maskL.mode === "req") {
|
|
468
|
+
const pick = (key, all, head, opts) => {
|
|
469
|
+
const b = el("button", "sess-pick");
|
|
470
|
+
b.type = "button";
|
|
471
|
+
if (maskL[key] && !opts.some((o) => o.v === maskL[key])) opts.unshift({ v: maskL[key], name: maskWho(maskL[key], key).name });
|
|
472
|
+
sessPick(b, all, maskL[key], opts, head, (v) => maskNarrow({ [key]: v || "" }));
|
|
473
|
+
bar.append(b);
|
|
474
|
+
};
|
|
475
|
+
pick("agent", "Every agent", "Agent", (sum?.agents || []).map((a) => ({ v: a.name, name: maskWho(a.name, "agent").name })));
|
|
476
|
+
pick("provider", "Every provider", "Provider", (sum?.providers || []).map((p) => ({ v: p.name, name: maskWho(p.name, "provider").name })));
|
|
477
|
+
}
|
|
478
|
+
box.append(bar);
|
|
479
|
+
|
|
480
|
+
// what was masked and how: counts that narrow the list
|
|
481
|
+
const facets = el("div", "msk-facets");
|
|
482
|
+
facets.setAttribute("role", "group");
|
|
483
|
+
facets.setAttribute("aria-label", t("Show"));
|
|
484
|
+
for (const [id, name, count] of MASK_FACETS) {
|
|
485
|
+
const n = sum ? count(sum) : 0;
|
|
486
|
+
if (id && !n && maskL.facet !== id) continue;
|
|
487
|
+
const b = el("button", "msk-facet" + (id ? " " + id : ""));
|
|
488
|
+
b.type = "button";
|
|
489
|
+
b.setAttribute("aria-pressed", String(maskL.facet === id && !maskL.kind));
|
|
490
|
+
if (id) b.append(el("span", "msk-fdot"));
|
|
491
|
+
b.append(el("span", "", t(name)));
|
|
492
|
+
if (sum) b.append(el("b", "", fmtN(n)));
|
|
493
|
+
b.onclick = () => maskNarrow({ facet: id, kind: "" });
|
|
494
|
+
facets.append(b);
|
|
495
|
+
}
|
|
496
|
+
if (maskL.kind) {
|
|
497
|
+
const b = el("button", "msk-facet kind");
|
|
498
|
+
b.type = "button";
|
|
499
|
+
b.setAttribute("aria-pressed", "true");
|
|
500
|
+
b.title = t("Clear filter");
|
|
501
|
+
b.append(maskDot(maskL.kind), el("span", "", maskKindName(maskL.kind)), el("span", "x", "×"));
|
|
502
|
+
b.onclick = () => maskNarrow({ kind: "" });
|
|
503
|
+
facets.append(b);
|
|
504
|
+
}
|
|
505
|
+
box.append(facets);
|
|
506
|
+
if (maskL.mode === "val") maskValueList(box, sum);
|
|
507
|
+
else maskRequestList(box);
|
|
508
|
+
const foot = el("div", "msk-foot");
|
|
509
|
+
foot.append(el("span", "", t("Kept on this computer for 30 days, without the values themselves")));
|
|
510
|
+
if (maskL.mode === "req" && maskL.more) {
|
|
511
|
+
const more = el("button", "text", maskL.busy ? t("Loading…") : t("Load more"));
|
|
512
|
+
more.type = "button";
|
|
513
|
+
more.disabled = maskL.busy;
|
|
514
|
+
more.onclick = () => maskFetch(true);
|
|
515
|
+
foot.append(more);
|
|
516
|
+
}
|
|
517
|
+
// what was kept goes whole, after a confirm: maxyou2090's strict masking
|
|
518
|
+
// had kept far more than he wanted to keep
|
|
519
|
+
if (maskL.records.length || maskSum?.requests) {
|
|
520
|
+
const clear = el("button", "text msk-clear", t("Clear records…"));
|
|
521
|
+
clear.type = "button";
|
|
522
|
+
clear.disabled = maskL.busy || maskClearing;
|
|
523
|
+
clear.onclick = maskClear;
|
|
524
|
+
foot.append(clear);
|
|
525
|
+
}
|
|
526
|
+
box.append(foot);
|
|
527
|
+
}
|
|
528
|
+
let maskClearing = false;
|
|
529
|
+
async function maskClear() {
|
|
530
|
+
const ok = await confirmAction(t("Clear masking records?"),
|
|
531
|
+
t("Every request's record of what was masked is deleted from this computer. Masking stays as it is, and requests from now on are recorded again."),
|
|
532
|
+
t("Clear"));
|
|
533
|
+
if (!ok) return;
|
|
534
|
+
maskClearing = true;
|
|
535
|
+
drawMaskList();
|
|
536
|
+
try {
|
|
537
|
+
await api("masking/clear", {});
|
|
538
|
+
status(t("Masking records cleared"), "ok", 1500);
|
|
539
|
+
} catch (e) {
|
|
540
|
+
// what could not be deleted is still listed below
|
|
541
|
+
status(t(e.message), "err");
|
|
542
|
+
}
|
|
543
|
+
maskClearing = false;
|
|
544
|
+
if (maskO) maskOneClose();
|
|
545
|
+
maskSeq++;
|
|
546
|
+
Object.assign(maskL, { records: [], more: false, busy: false, read: false, error: "" });
|
|
547
|
+
maskDrawn = "";
|
|
548
|
+
return maskPageLoad(true);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function maskRequestList(box) {
|
|
552
|
+
const filtered = maskL.facet || maskL.kind || maskL.agent || maskL.provider;
|
|
553
|
+
if (maskL.error) return void box.append(el("div", "msk-none err", t(maskL.error)));
|
|
554
|
+
if (!maskL.records.length) return void box.append(el("div", "msk-none", maskL.busy || !maskL.read ? t("Loading…") : filtered ? t("Nothing masked matches these filters") : t("Nothing masked yet")));
|
|
555
|
+
let day = "", list = null;
|
|
556
|
+
for (const r of maskL.records) {
|
|
557
|
+
const d = new Date(r.t).toDateString();
|
|
558
|
+
if (d !== day) {
|
|
559
|
+
day = d;
|
|
560
|
+
const g = el("div", "msk-group");
|
|
561
|
+
g.append(el("div", "msk-gday", maskDayName(r.t)));
|
|
562
|
+
list = el("div", "list");
|
|
563
|
+
g.append(list);
|
|
564
|
+
box.append(g);
|
|
565
|
+
}
|
|
566
|
+
list.append(maskRecordRow(r));
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// who to whom, an icon where there is one
|
|
570
|
+
function maskRoute(into, r) {
|
|
571
|
+
const a = maskWho(r.agent, "agent"), p = maskWho(r.provider, "provider");
|
|
572
|
+
if (r.agent) {
|
|
573
|
+
if (a.icon) into.append(icon(a.icon));
|
|
574
|
+
into.append(el("span", "who", a.name), el("span", "arrow", "→"));
|
|
575
|
+
}
|
|
576
|
+
if (p.icon && r.provider) into.append(icon(p.icon));
|
|
577
|
+
into.append(el("span", "who", r.provider ? p.name : t("A vendor")));
|
|
578
|
+
}
|
|
579
|
+
function maskKindsOf(r) {
|
|
580
|
+
const by = {};
|
|
581
|
+
for (const x of r.values || []) by[x.k] = (by[x.k] || 0) + (x.n || 1);
|
|
582
|
+
return Object.entries(by).sort((x, y) => y[1] - x[1]);
|
|
583
|
+
}
|
|
584
|
+
function maskRecordRow(r) {
|
|
585
|
+
const row = el("button", "msk-row" + (r.blocked ? " held" : ""));
|
|
586
|
+
row.type = "button";
|
|
587
|
+
row.dataset.key = r.key;
|
|
588
|
+
const route = el("span", "msk-route");
|
|
589
|
+
maskRoute(route, r);
|
|
590
|
+
const model = el("span", "msk-model", r.model || "");
|
|
591
|
+
const kinds = el("span", "msk-kinds");
|
|
592
|
+
if (r.blocked) kinds.append(el("span", "msk-held", t("Held back")));
|
|
593
|
+
else if (r.what && r.what !== "chat") kinds.append(el("span", "msk-what", MASK_WHAT[r.what] ? t(MASK_WHAT[r.what]) : r.what));
|
|
594
|
+
const ranked = maskKindsOf(r);
|
|
595
|
+
for (const [k, n] of ranked.slice(0, 3)) {
|
|
596
|
+
const c = el("span", "msk-kind");
|
|
597
|
+
c.append(maskDot(k), el("span", "", maskKindName(k)), el("b", "", "×" + fmtN(n)));
|
|
598
|
+
kinds.append(c);
|
|
599
|
+
}
|
|
600
|
+
if (ranked.length > 3) kinds.append(el("span", "msk-kind more", "+" + (ranked.length - 3)));
|
|
601
|
+
const n = el("span", "msk-n", fmtN(r.n));
|
|
602
|
+
n.title = t(r.n === 1 ? "1 value masked" : "{n} values masked", { n: fmtN(r.n) });
|
|
603
|
+
const chev = el("span", "msk-chev");
|
|
604
|
+
chev.append(svg(CHEV_R, 11, 1.6));
|
|
605
|
+
row.append(el("span", "msk-time", new Date(r.t).toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit" })), route, model, kinds, n, chev);
|
|
606
|
+
row.onclick = () => maskOneOpen(r.key, r);
|
|
607
|
+
return row;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// the values masked most, as the summary ranks them: what a facet can
|
|
611
|
+
// narrow them by; one opens the requests with its kind
|
|
612
|
+
function maskValueList(box, sum) {
|
|
613
|
+
const f = maskL.facet;
|
|
614
|
+
const vs = (sum?.values || []).filter((v) => (!maskL.kind || v.kind === maskL.kind) && (f !== "secret" || MASK_SECRET_KINDS.includes(v.kind)) && (f !== "rule" && f !== "model" || v.by === f) && f !== "blocked");
|
|
615
|
+
if (!sum) return void box.append(el("div", "msk-none", t("Loading…")));
|
|
616
|
+
if (!vs.length) return void box.append(el("div", "msk-none", sum.values?.length ? t("Nothing masked matches these filters") : t("Nothing masked yet")));
|
|
617
|
+
const head = el("div", "msk-vhead");
|
|
618
|
+
head.append(el("span", "", t("Masked most")), el("span", "grow"), el("span", "", t("Point at one to see what it was")));
|
|
619
|
+
const list = el("div", "list msk-vals"), max = vs[0].n || 1;
|
|
620
|
+
for (const [i, v] of vs.entries()) {
|
|
621
|
+
const c = el("button", "msk-vrow"), sh = maskShape(v.head, v.len), bar = el("span", "bar"), fill = el("i");
|
|
622
|
+
c.type = "button";
|
|
623
|
+
c.setAttribute("aria-label", maskKindName(v.kind) + " " + sh);
|
|
624
|
+
fill.style.width = Math.max(3, (100 * v.n) / max) + "%";
|
|
625
|
+
bar.append(fill);
|
|
626
|
+
const kind = el("span", "k");
|
|
627
|
+
kind.append(maskDot(v.kind), el("span", "", maskKindName(v.kind)));
|
|
628
|
+
c.append(el("span", "i", String(i + 1).padStart(2, "0")), el("span", "sh", sh), kind, el("span", "by", maskByName(v.by)), bar, el("span", "x", fmtN(v.n)));
|
|
629
|
+
maskTipOn(c, { p: v.p, kind: v.kind, by: v.by, meta: t(v.requests === 1 ? "In 1 request, {when}" : "In {n} requests, most recently {when}", { n: fmtN(v.requests), when: ago(v.last) }) });
|
|
630
|
+
c.onclick = () => { maskL.mode = "req"; maskNarrow({ kind: v.kind, facet: "" }); };
|
|
631
|
+
list.append(c);
|
|
632
|
+
}
|
|
633
|
+
box.append(head, list);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ---- one request: what it carried, what the vendor got, how ----
|
|
637
|
+
function maskOneOpen(key, r) {
|
|
638
|
+
if (!key) return;
|
|
639
|
+
maskTipOff();
|
|
640
|
+
const o = maskO = { key, r: r || maskL.records.find((x) => x.key === key) || null, sent: undefined, leaves: null, vals: new Map(), view: "both", pick: "", open: new Set(), whole: new Set() };
|
|
641
|
+
$("#maskCard").hidden = $("#maskList").hidden = $("#maskDemo").hidden = $("#maskPrefs").hidden = true;
|
|
642
|
+
xqd?.wake();
|
|
643
|
+
$("#maskOne").hidden = false;
|
|
644
|
+
drawMaskOne();
|
|
645
|
+
(o.r ? Promise.resolve(o.r) : api("masking/record?key=" + key))
|
|
646
|
+
.then((rec) => {
|
|
647
|
+
if (maskO !== o) return;
|
|
648
|
+
o.r = rec;
|
|
649
|
+
drawMaskOne();
|
|
650
|
+
const ps = (rec.values || []).map((x) => x.p);
|
|
651
|
+
const vals = ps.length ? api("masking/values", { ps }).then((x) => {
|
|
652
|
+
for (const [p, v] of Object.entries(x?.values || {})) { o.vals.set(p, v); maskValues.set(p, v); }
|
|
653
|
+
}).catch(() => {}) : null;
|
|
654
|
+
const sent = api("masking/sent?key=" + key).then((x) => (typeof x?.body === "string" ? x.body : null), () => null);
|
|
655
|
+
return Promise.all([sent, vals]).then(([body]) => {
|
|
656
|
+
if (maskO !== o) return;
|
|
657
|
+
o.sent = body;
|
|
658
|
+
o.leaves = body === null ? null : maskLeaves(body);
|
|
659
|
+
if (body === null && !o.vals.size) o.view = "vendor";
|
|
660
|
+
drawMaskOne();
|
|
661
|
+
});
|
|
662
|
+
})
|
|
663
|
+
.catch(() => { if (maskO === o) { o.r = false; drawMaskOne(); } });
|
|
664
|
+
}
|
|
665
|
+
function maskOneClose() {
|
|
666
|
+
maskTipOff();
|
|
667
|
+
maskO = null;
|
|
668
|
+
$("#maskOne").hidden = true;
|
|
669
|
+
$("#maskOne").replaceChildren();
|
|
670
|
+
$("#maskCard").hidden = $("#maskList").hidden = $("#maskDemo").hidden = false;
|
|
671
|
+
$("#maskPrefs").hidden = gatewayMode;
|
|
672
|
+
xqd?.wake();
|
|
673
|
+
drawMaskList();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// maskLeaves: each piece of text in the body a request went out with,
|
|
677
|
+
// where it sits and whose it is (the message's role, else its type)
|
|
678
|
+
function maskLeaves(body) {
|
|
679
|
+
let root;
|
|
680
|
+
try { root = JSON.parse(body); } catch { return [{ path: "", role: "", text: body }]; }
|
|
681
|
+
const out = [];
|
|
682
|
+
const walk = (v, path, role) => {
|
|
683
|
+
if (typeof v === "string") return void out.push({ path, role, text: v });
|
|
684
|
+
if (!v || typeof v !== "object") return;
|
|
685
|
+
if (Array.isArray(v)) return void v.forEach((x, i) => walk(x, path + "[" + i + "]", role));
|
|
686
|
+
const r = typeof v.role === "string" ? v.role : role || (typeof v.type === "string" ? v.type : "");
|
|
687
|
+
for (const [k, x] of Object.entries(v)) walk(x, path ? path + "." + k : k, r);
|
|
688
|
+
};
|
|
689
|
+
walk(root, "", "");
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
const maskTook = (us) => us >= 1e6 ? (us / 1e6).toFixed(1) + " s" : us >= 1000 ? (us / 1000).toFixed(us >= 1e4 ? 0 : 1) + " ms" : Math.max(1, Math.round(us)) + " µs";
|
|
693
|
+
|
|
694
|
+
function drawMaskOne() {
|
|
695
|
+
const box = $("#maskOne"), o = maskO;
|
|
696
|
+
if (!box || !o) return;
|
|
697
|
+
maskTipOff();
|
|
698
|
+
box.replaceChildren();
|
|
699
|
+
const r = o.r;
|
|
700
|
+
// back, when, and the requests beside it in the list
|
|
701
|
+
const bar = el("div", "msk-onebar");
|
|
702
|
+
const back = el("button", "text msk-back");
|
|
703
|
+
back.type = "button";
|
|
704
|
+
back.append(svg("m9.5 3.5-4.5 4.5 4.5 4.5", 12, 1.7), el("span", "", t("Masking")));
|
|
705
|
+
back.onclick = maskOneClose;
|
|
706
|
+
bar.append(back);
|
|
707
|
+
if (r) bar.append(el("span", "msk-crumb", "/"), el("span", "msk-when", maskDayName(r.t) + " " + new Date(r.t).toLocaleTimeString(intlLang(), { hour: "2-digit", minute: "2-digit", second: "2-digit" })));
|
|
708
|
+
bar.append(el("span", "grow"));
|
|
709
|
+
const at = maskL.records.findIndex((x) => x.key === o.key);
|
|
710
|
+
const step = (d, label, path) => {
|
|
711
|
+
const b = el("button", "icon msk-step");
|
|
712
|
+
b.type = "button";
|
|
713
|
+
b.setAttribute("aria-label", t(label));
|
|
714
|
+
b.dataset.tt = "";
|
|
715
|
+
b.append(svg(path, 12, 1.7));
|
|
716
|
+
const to = at < 0 ? null : maskL.records[at + d];
|
|
717
|
+
b.disabled = !to;
|
|
718
|
+
if (to) b.onclick = () => maskOneOpen(to.key, to);
|
|
719
|
+
bar.append(b);
|
|
720
|
+
};
|
|
721
|
+
step(-1, "Newer request", "M3.5 9.5 8 5l4.5 4.5");
|
|
722
|
+
step(1, "Older request", "M3.5 6.5 8 11l4.5-4.5");
|
|
723
|
+
box.append(bar);
|
|
724
|
+
if (r === false) return void box.append(el("div", "msk-none", t("This request's record is gone: records are kept for 30 days")));
|
|
725
|
+
if (!r) return void box.append(el("div", "msk-none", t("Loading…")));
|
|
726
|
+
|
|
727
|
+
const info = new Map((r.values || []).map((x) => [x.p, x]));
|
|
728
|
+
// the request: who to whom, and what happened to it in order
|
|
729
|
+
const card = el("section", "msk-req");
|
|
730
|
+
const who = el("div", "msk-who");
|
|
731
|
+
maskRoute(who, r);
|
|
732
|
+
if (r.model) who.append(el("span", "msk-chip", r.model));
|
|
733
|
+
if (r.what && r.what !== "chat") who.append(el("span", "msk-chip", MASK_WHAT[r.what] ? t(MASK_WHAT[r.what]) : r.what));
|
|
734
|
+
card.append(who, maskSteps(r, o));
|
|
735
|
+
box.append(card);
|
|
736
|
+
if (r.blocked) box.append(maskBlocked(r));
|
|
737
|
+
|
|
738
|
+
const grid = el("div", "msk-onegrid");
|
|
739
|
+
grid.append(maskContent(r, o, info), maskHow(r, o, info));
|
|
740
|
+
box.append(grid);
|
|
741
|
+
maskPick(box, o.pick);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// maskSteps: received, found, replaced, sent (or held back), put back
|
|
745
|
+
function maskSteps(r, o) {
|
|
746
|
+
const ol = el("ol", "msk-steps");
|
|
747
|
+
ol.setAttribute("aria-label", t("What happened"));
|
|
748
|
+
const memory = o.sent || o.vals.size || Object.keys(r.snips || {}).length;
|
|
749
|
+
const back = Object.values(r.back || {}).reduce((a, n) => a + n, 0);
|
|
750
|
+
const by = [...new Set((r.values || []).map((x) => maskByName(x.b)))].join(", ");
|
|
751
|
+
const a = maskWho(r.agent, "agent").name, p = r.provider ? maskWho(r.provider, "provider").name : t("A vendor");
|
|
752
|
+
const steps = [
|
|
753
|
+
["", t("Received"), r.agent ? t("From {agent}", { agent: a }) + (o.sent ? " · " + fmtBytes(new Blob([o.sent]).size) : "") : o.sent ? fmtBytes(new Blob([o.sent]).size) : ""],
|
|
754
|
+
["", t((r.values || []).length === 1 ? "Found 1 value" : "Found {n} values", { n: fmtN((r.values || []).length) }), by + (r.took ? " · " + maskTook(r.took) : "")],
|
|
755
|
+
["", t(r.n === 1 ? "Replaced 1 place" : "Replaced {n} places", { n: fmtN(r.n) }), t("With placeholders")],
|
|
756
|
+
r.blocked ? ["held", t("Held back"), t("Not sent to {provider}", { provider: p })] : ["", t("Sent"), p + (r.model ? " · " + r.model : "")],
|
|
757
|
+
r.blocked ? ["none", t("Put back"), t("Nothing was sent")]
|
|
758
|
+
: back ? ["", t(back === 1 ? "Put back 1" : "Put back {n}", { n: fmtN(back) }), t("In the answer, before {agent} got it", { agent: a })]
|
|
759
|
+
: memory ? ["none", t("Nothing to put back"), t("The answer named no placeholder")]
|
|
760
|
+
: ["none", t("Put back"), t("Not known after a restart")],
|
|
761
|
+
];
|
|
762
|
+
steps.forEach(([cls, title, sub], i) => {
|
|
763
|
+
const li = el("li", cls);
|
|
764
|
+
const mk = el("div", "msk-sdot");
|
|
765
|
+
mk.append(el("span", "", String(i + 1)));
|
|
766
|
+
li.append(mk, el("div", "msk-st", title), el("div", "msk-ss", sub));
|
|
767
|
+
ol.append(li);
|
|
768
|
+
});
|
|
769
|
+
return ol;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// a request held back: why, and the way to send it masked instead
|
|
773
|
+
function maskBlocked(r) {
|
|
774
|
+
const box = el("section", "msk-blocked");
|
|
775
|
+
const head = el("div", "msk-bhead"), ic = svg(SHIELD, 20, 1.3);
|
|
776
|
+
ic.append(sv("path", { d: "m6 6 4 4M10 6l-4 4" }));
|
|
777
|
+
head.append(ic, el("h2", "", t("This request was not sent")));
|
|
778
|
+
const kinds = [...new Set((r.values || []).filter((x) => x.a === "block").map((x) => x.k))];
|
|
779
|
+
const a = maskWho(r.agent, "agent").name, p = r.provider ? maskWho(r.provider, "provider").name : t("A vendor");
|
|
780
|
+
box.append(head, el("p", "", t("It carried {kinds}, which you set to hold back, so magpie didn't send it to {provider}. {agent} got an error saying why.", { kinds: kinds.map(maskKindName).join(", ") || t("a value"), provider: p, agent: r.agent ? a : t("The agent") })));
|
|
781
|
+
const tools = el("div", "msk-btools");
|
|
782
|
+
if (kinds.length) {
|
|
783
|
+
const b = el("button", "text primary", t("Mask it instead"));
|
|
784
|
+
b.type = "button";
|
|
785
|
+
b.title = t("From now on {kinds} go to vendors as placeholders, like the other values", { kinds: kinds.map(maskKindName).join(", ") });
|
|
786
|
+
b.onclick = () => {
|
|
787
|
+
b.disabled = true;
|
|
788
|
+
(prefs ? Promise.resolve() : loadSettings())
|
|
789
|
+
.then(() => writingPrefs(api("settings/redact-actions", { actions: { ...(prefs.redactActions || {}), ...Object.fromEntries(kinds.map((k) => [k, "mask"])) } })))
|
|
790
|
+
.then((ns) => { prefs = ns; if (state) state.settings = ns; b.textContent = t("Masked from now on"); status(t("Saved"), "ok", 1500); })
|
|
791
|
+
.catch((e) => { b.disabled = false; status(t(e.message), "err"); });
|
|
792
|
+
};
|
|
793
|
+
tools.append(b);
|
|
794
|
+
}
|
|
795
|
+
const rules = el("button", "text", t("See the rules in Settings"));
|
|
796
|
+
rules.type = "button";
|
|
797
|
+
rules.onclick = (e) => openSettingsAt(e, "privacy", $("#maskActList"));
|
|
798
|
+
tools.append(rules);
|
|
799
|
+
box.append(tools);
|
|
800
|
+
return box;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// maskInk: text with this request's placeholders marked, as the vendor
|
|
804
|
+
// got them or, here, as they were written
|
|
805
|
+
function maskInk(into, text, here, o, info) {
|
|
806
|
+
let at = 0;
|
|
807
|
+
for (const m of maskMatches(text, info)) {
|
|
808
|
+
const x = info.get(m[0]);
|
|
809
|
+
if (!x) continue;
|
|
810
|
+
into.append(document.createTextNode(text.slice(at, m.index)));
|
|
811
|
+
let mark;
|
|
812
|
+
if (here) {
|
|
813
|
+
const v = o.vals.get(m[0]);
|
|
814
|
+
mark = el("mark", "msk-orig" + (typeof v === "string" ? "" : " gone"), typeof v === "string" ? v : maskShape(x.h, x.l));
|
|
815
|
+
mark.style.setProperty("--k", maskColor(x.k));
|
|
816
|
+
mark.tabIndex = 0;
|
|
817
|
+
maskTipOn(mark, { p: m[0], kind: x.k, by: x.b });
|
|
818
|
+
} else mark = maskPlace("msk-ph", m[0], x);
|
|
819
|
+
mark.dataset.p = m[0];
|
|
820
|
+
into.append(mark);
|
|
821
|
+
at = m.index + m[0].length;
|
|
822
|
+
}
|
|
823
|
+
into.append(document.createTextNode(text.slice(at)));
|
|
824
|
+
}
|
|
825
|
+
// the parts of a long text around its placeholders, 120 characters each
|
|
826
|
+
// side, the rest left out
|
|
827
|
+
function maskWindows(text, info) {
|
|
828
|
+
const rs = [];
|
|
829
|
+
for (const m of maskMatches(text, info)) if (info.has(m[0])) rs.push([Math.max(0, m.index - 120), Math.min(text.length, m.index + m[0].length + 120)]);
|
|
830
|
+
const out = [];
|
|
831
|
+
for (const r of rs) {
|
|
832
|
+
const last = out.at(-1);
|
|
833
|
+
if (last && r[0] <= last[1] + 20) last[1] = Math.max(last[1], r[1]);
|
|
834
|
+
else out.push([...r]);
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// maskContent: the request's text, only the pieces masking changed, the
|
|
840
|
+
// rest folded; as written, as the vendor got it, or side by side
|
|
841
|
+
function maskContent(r, o, info) {
|
|
842
|
+
const sec = el("section", "msk-content");
|
|
843
|
+
const head = el("div", "msk-chead");
|
|
844
|
+
head.append(el("h2", "", t("Request content")), el("span", "msk-hint", t("Only the parts masking changed")), el("span", "grow"));
|
|
845
|
+
const known = o.sent || o.vals.size;
|
|
846
|
+
const views = [["here", t("Original")], ["vendor", t("What the vendor saw")], ["both", t("Side by side")]];
|
|
847
|
+
const sg = segs(views, o.view, (v) => { o.view = v; drawMaskOne(); });
|
|
848
|
+
if (!known && o.sent !== undefined) for (const b of sg.querySelectorAll("button")) if (!b.getAttribute("aria-pressed") || b.getAttribute("aria-pressed") === "false") b.disabled = true;
|
|
849
|
+
head.append(sg);
|
|
850
|
+
sec.append(head);
|
|
851
|
+
const view = known || o.sent === undefined ? o.view : "vendor";
|
|
852
|
+
if (o.sent === undefined) return sec.append(el("div", "msk-none", t("Loading…"))), sec;
|
|
853
|
+
|
|
854
|
+
let items;
|
|
855
|
+
if (o.leaves) {
|
|
856
|
+
items = [];
|
|
857
|
+
let fold = null;
|
|
858
|
+
for (const l of o.leaves) {
|
|
859
|
+
const changed = [...maskMatches(l.text, info)].some((m) => info.has(m[0]));
|
|
860
|
+
if (changed) { fold = null; items.push({ leaf: l }); continue; }
|
|
861
|
+
if (!fold) items.push(fold = { fold: [] });
|
|
862
|
+
fold.fold.push(l);
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
// only the lines around each value are left, while memory has the
|
|
866
|
+
// record: the request's whole text was in memory, and is no longer
|
|
867
|
+
items = maskSnipTexts(r).map((text) => ({ leaf: { path: "", role: "", text } }));
|
|
868
|
+
if (items.length && !r.blocked) sec.append(el("p", "msk-gone", o.vals.size
|
|
869
|
+
? t("This request's whole text is no longer in memory: magpie keeps it for the newest requests only. The lines around each value are still here.")
|
|
870
|
+
: t("What was written is kept in memory only, and magpie has restarted since. Still here: each value's kind, length and first characters, and the line around each placeholder as the vendor got it.")));
|
|
871
|
+
// after a restart nothing of the text is left, by design: the record on
|
|
872
|
+
// disk never holds it (maskrec), so no folder or volume brings it back
|
|
873
|
+
// (brianeee2 on X, in Docker). A request held back sent nothing, and
|
|
874
|
+
// its card says so.
|
|
875
|
+
else if (!items.length && !r.blocked) sec.append(el("p", "msk-gone", t("This request's text and the values masked in it were kept in memory only, never on disk, and magpie has restarted since. The record keeps each value's kind, length and first characters.")));
|
|
876
|
+
}
|
|
877
|
+
const list = el("div", "msk-msgs" + (view === "both" ? " both" : ""));
|
|
878
|
+
if (view === "both") {
|
|
879
|
+
const cols = el("div", "msk-cols");
|
|
880
|
+
cols.append(el("span", "", t("As written here")), el("span", "", t("What the vendor saw")));
|
|
881
|
+
list.append(cols);
|
|
882
|
+
}
|
|
883
|
+
items.forEach((it, i) => {
|
|
884
|
+
if (it.fold) {
|
|
885
|
+
const size = it.fold.reduce((a, l) => a + l.text.length, 0);
|
|
886
|
+
const f = el("div", "msk-fold" + (o.open.has(i) ? " open" : ""));
|
|
887
|
+
const b = el("button", "msk-foldb");
|
|
888
|
+
b.type = "button";
|
|
889
|
+
b.setAttribute("aria-expanded", String(o.open.has(i)));
|
|
890
|
+
b.append(svg(CHEV_R, 10, 1.8), el("span", "", t(it.fold.length === 1 ? "1 part unchanged" : "{n} parts unchanged", { n: fmtN(it.fold.length) })), el("span", "msk-size", fmtBytes(size)));
|
|
891
|
+
b.onclick = () => { if (o.open.has(i)) o.open.delete(i); else o.open.add(i); drawMaskOne(); };
|
|
892
|
+
f.append(b);
|
|
893
|
+
if (o.open.has(i)) {
|
|
894
|
+
const rest = el("div", "msk-same");
|
|
895
|
+
for (const l of it.fold.slice(0, 40)) {
|
|
896
|
+
const p = el("div", "msk-sameline");
|
|
897
|
+
p.append(el("span", "msk-path", l.path), el("span", "", l.text.length > 400 ? l.text.slice(0, 400) + "…" : l.text));
|
|
898
|
+
rest.append(p);
|
|
899
|
+
}
|
|
900
|
+
if (it.fold.length > 40) rest.append(el("div", "msk-path", t("{n} more", { n: fmtN(it.fold.length - 40) })));
|
|
901
|
+
f.append(rest);
|
|
902
|
+
}
|
|
903
|
+
return list.append(f);
|
|
904
|
+
}
|
|
905
|
+
const l = it.leaf, m = el("div", "msk-msg");
|
|
906
|
+
const n = [...maskMatches(l.text, info)].filter((x) => info.has(x[0])).length;
|
|
907
|
+
const mh = el("div", "msk-mhead");
|
|
908
|
+
if (l.role) mh.append(el("span", "msk-role", l.role));
|
|
909
|
+
if (l.path) mh.append(el("span", "msk-path", l.path));
|
|
910
|
+
mh.append(el("span", "grow"), el("span", "msk-count", t(n === 1 ? "1 value" : "{n} values", { n: fmtN(n) })));
|
|
911
|
+
m.append(mh);
|
|
912
|
+
const whole = o.whole.has(i), wins = whole || l.text.length <= 600 ? [[0, l.text.length]] : maskWindows(l.text, info);
|
|
913
|
+
const col = (here) => {
|
|
914
|
+
const c = el("div", "msk-text");
|
|
915
|
+
wins.forEach(([a, b], j) => {
|
|
916
|
+
if (j || a) c.append(el("span", "msk-gap", "…"));
|
|
917
|
+
maskInk(c, l.text.slice(a, b), here, o, info);
|
|
918
|
+
});
|
|
919
|
+
if (wins.at(-1)[1] < l.text.length) c.append(el("span", "msk-gap", "…"));
|
|
920
|
+
return c;
|
|
921
|
+
};
|
|
922
|
+
const body = el("div", "msk-mbody");
|
|
923
|
+
if (view === "both") body.append(col(true), col(false));
|
|
924
|
+
else body.append(col(view === "here"));
|
|
925
|
+
m.append(body);
|
|
926
|
+
if (l.text.length > 600) {
|
|
927
|
+
const b = el("button", "text msk-whole", whole ? t("Show only the changes") : t("Show all of it"));
|
|
928
|
+
b.type = "button";
|
|
929
|
+
b.onclick = () => { if (whole) o.whole.delete(i); else o.whole.add(i); drawMaskOne(); };
|
|
930
|
+
m.append(b);
|
|
931
|
+
}
|
|
932
|
+
list.append(m);
|
|
933
|
+
});
|
|
934
|
+
sec.append(list);
|
|
935
|
+
return sec;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// maskHow: each value, what it became, who found it, where it was, and
|
|
939
|
+
// how often the answer had it put back; picked, its places light up
|
|
940
|
+
function maskHow(r, o, info) {
|
|
941
|
+
const aside = el("aside", "msk-how");
|
|
942
|
+
const vs = r.values || [];
|
|
943
|
+
const head = el("div", "msk-chead");
|
|
944
|
+
head.append(el("h2", "", t("How it was masked")), el("span", "msk-hint", t(vs.length === 1 ? "1 value" : "{n} values", { n: fmtN(vs.length) }) + " · " + t(r.n === 1 ? "1 place" : "{n} places", { n: fmtN(r.n) })));
|
|
945
|
+
aside.append(head);
|
|
946
|
+
const where = new Map();
|
|
947
|
+
for (const l of o.leaves || []) for (const m of maskMatches(l.text, info)) if (info.has(m[0]) && l.path) {
|
|
948
|
+
const w = where.get(m[0]) || new Set();
|
|
949
|
+
w.add(l.path);
|
|
950
|
+
where.set(m[0], w);
|
|
951
|
+
}
|
|
952
|
+
const memory = o.sent || o.vals.size;
|
|
953
|
+
for (const x of vs) {
|
|
954
|
+
const b = el("button", "msk-val");
|
|
955
|
+
b.type = "button";
|
|
956
|
+
b.dataset.p = x.p;
|
|
957
|
+
b.style.setProperty("--k", maskColor(x.k));
|
|
958
|
+
b.setAttribute("aria-pressed", String(o.pick === x.p));
|
|
959
|
+
const top = el("div", "msk-vtop");
|
|
960
|
+
top.append(maskDot(x.k), el("span", "msk-vk", maskKindName(x.k)), el("span", "grow"));
|
|
961
|
+
if (x.a === "warn") top.append(el("span", "mr-act warn", t("Sent as written")));
|
|
962
|
+
else if (x.a === "block") top.append(el("span", "mr-act block", t("Held back")));
|
|
963
|
+
top.append(el("span", "msk-vx", "×" + fmtN(x.n || 1)));
|
|
964
|
+
const v = o.vals.get(x.p), shown = o.pick === x.p && typeof v === "string" ? v : maskShape(x.h, x.l);
|
|
965
|
+
const mid = el("div", "msk-vmid");
|
|
966
|
+
mid.append(el("span", "msk-vshown", shown), svg("M1 8h11M9 5l3 3-3 3", 12, 1.3), el("span", "msk-vph", x.a === "warn" ? t("as written") : x.p));
|
|
967
|
+
const meta = el("dl", "msk-vmeta");
|
|
968
|
+
const add = (k, val) => meta.append(el("dt", "", k), el("dd", "", val));
|
|
969
|
+
add(t("Found by"), maskByName(x.b));
|
|
970
|
+
const w = [...(where.get(x.p) || [])];
|
|
971
|
+
if (w.length) add(t("Where"), w[0] + (w.length > 1 ? " +" + (w.length - 1) : ""));
|
|
972
|
+
const n = r.back?.[x.p] || 0;
|
|
973
|
+
add(t("In the answer"), n ? t(n === 1 ? "Put back 1" : "Put back {n}", { n: fmtN(n) }) : r.blocked ? "—" : memory || r.back ? t("Not named") : t("Not known after a restart"));
|
|
974
|
+
b.append(top, mid, meta);
|
|
975
|
+
b.onclick = () => { o.pick = o.pick === x.p ? "" : x.p; drawMaskOne(); };
|
|
976
|
+
aside.append(b);
|
|
977
|
+
}
|
|
978
|
+
aside.append(el("p", "msk-note", t("A value itself is in this computer's memory only: shown while it is pointed at or picked, and gone once magpie restarts. The record on disk keeps its kind, length and first characters.")));
|
|
979
|
+
return aside;
|
|
980
|
+
}
|
|
981
|
+
// a value picked lights its places; the rest fade
|
|
982
|
+
function maskPick(box, p) {
|
|
983
|
+
box.classList.toggle("picking", !!p);
|
|
984
|
+
for (const m of box.querySelectorAll("mark[data-p]")) m.classList.toggle("lit", m.dataset.p === p);
|
|
985
|
+
}
|