cotutor 0.3.0 → 0.4.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/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +36 -13
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +5 -0
- package/dist/app/cli/cert.js +20 -7
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +6 -5
- package/dist/app/cli/doctor.js +107 -171
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +347 -24
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +18 -4
- package/dist/app/cli/serve.js +78 -8
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +138 -0
- package/dist/app/cli/service.js +429 -0
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +33 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +205 -0
- package/dist/app/cli/tray.js +618 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +45 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +315 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +2 -1
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** 地址:默认端口不写(https 的 443、http 的 80)。不用 new URL:它会把主机名压成小写 */
|
|
2
|
+
export function siteUrl(scheme, host, port, path = '/') {
|
|
3
|
+
const isDefault = (scheme === 'https' && port === 443) || (scheme === 'http' && port === 80);
|
|
4
|
+
return `${scheme}://${host}${isDefault ? '' : `:${port}`}${path}`;
|
|
5
|
+
}
|
package/dist/ui/dev/dev-page.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { faviconLinks } from "../shared/favicon.js";
|
|
12
12
|
import { DEV_CSS } from "./dev-theme.js";
|
|
13
13
|
import { pageDomJs } from "../shared/page-dom.js";
|
|
14
|
-
import { BRAND_CSS, BRAND_NAME, BRAND_URL, brandLogo } from "../shared/brand.js";
|
|
14
|
+
import { BRAND_CSS, BRAND_NAME, BRAND_URL, GITHUB_MARK, brandLogo } from "../shared/brand.js";
|
|
15
15
|
export const DEV_PAGE = `<!doctype html>
|
|
16
16
|
<html lang="zh-CN">
|
|
17
17
|
<meta charset="utf-8">
|
|
@@ -29,6 +29,13 @@ ${BRAND_CSS}
|
|
|
29
29
|
#side .brand .logo { width:30px; height:30px; border-radius:9px; background:var(--accent); display:grid; place-items:center; flex:none; }
|
|
30
30
|
#side .brand b { font-size:17px; font-weight:700; letter-spacing:-.01em; }
|
|
31
31
|
#side .brand small { font-size:13px; color:var(--muted); }
|
|
32
|
+
#side .brand-w { display:flex; flex-direction:column; gap:8px; }
|
|
33
|
+
#side .star { display:flex; align-items:center; gap:7px; margin:0 2px; padding:7px 10px; border-radius:var(--r-ctl); color:var(--ink-2); font-size:12.5px; white-space:nowrap; box-shadow:0 0 0 1px var(--line); }
|
|
34
|
+
#side .star:hover { background:var(--surface); color:var(--ink); }
|
|
35
|
+
#side .star svg { flex:none; }
|
|
36
|
+
#side .star b { font-weight:600; }
|
|
37
|
+
#side .star span { color:var(--muted); }
|
|
38
|
+
#side .star .out { margin-left:auto; font-style:normal; color:var(--muted); }
|
|
32
39
|
#side .ws { display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:var(--r-item); background:var(--surface); box-shadow:0 0 0 1px var(--line); min-width:0; }
|
|
33
40
|
#side .ws .ini { width:28px; height:28px; border-radius:50%; background:var(--sunk); display:grid; place-items:center; font-size:13px; font-weight:600; color:var(--ink-2); flex:none; }
|
|
34
41
|
#side .ws span { display:flex; flex-direction:column; min-width:0; }
|
|
@@ -394,33 +401,6 @@ ${BRAND_CSS}
|
|
|
394
401
|
.gaps li .plus { font:600 13px/1 var(--mono); color:var(--ok); }
|
|
395
402
|
.gaps li .minus { color:var(--muted); }
|
|
396
403
|
|
|
397
|
-
.field .withbtn { display:flex; gap:6px; align-items:center; }
|
|
398
|
-
.field .withbtn input { flex:1; min-width:0; }
|
|
399
|
-
.field .withbtn .btn { flex:none; height:36px; }
|
|
400
|
-
.voices header .for { display:flex; align-items:center; gap:6px; font-size:13px; color:var(--muted); white-space:nowrap; }
|
|
401
|
-
.voices header .for select { font-size:13.5px; height:34px; padding:0 8px; border:0; box-shadow:0 0 0 1px var(--line); border-radius:9px; background:var(--surface); max-width:180px; }
|
|
402
|
-
.voices .now { display:flex; align-items:center; gap:10px; padding:12px 20px; border-bottom:1px solid var(--line-soft); flex-wrap:wrap; }
|
|
403
|
-
.voices .now small { font:400 12px/1.4 var(--mono); color:var(--muted); }
|
|
404
|
-
.voices .now .muted { color:var(--muted); }
|
|
405
|
-
.handrow { display:flex; align-items:center; gap:10px; padding:12px 20px; flex-wrap:wrap; }
|
|
406
|
-
.handrow .muted { color:var(--muted); }
|
|
407
|
-
.voices .filters { display:flex; gap:8px; padding:12px 20px; border-bottom:1px solid var(--line-soft); flex-wrap:wrap; background:var(--surface-2); }
|
|
408
|
-
.voices .filters input, .voices .filters select { height:34px; font-size:13.5px; padding:0 10px; border:0; box-shadow:0 0 0 1px var(--line); border-radius:9px; background:var(--surface); }
|
|
409
|
-
.voices .filters input[type=search] { flex:1 1 220px; min-width:0; }
|
|
410
|
-
.voices .filters .sample { flex:1 1 100%; }
|
|
411
|
-
.voices .vlist { max-height:calc(100vh - 360px); overflow:auto; }
|
|
412
|
-
.vrow { display:grid; grid-template-columns:auto minmax(150px,1.3fr) 76px minmax(90px,1fr) minmax(80px,1fr) auto auto; align-items:center; gap:12px; padding:9px 20px; border-top:1px solid var(--line-soft); }
|
|
413
|
-
.vrow:first-child { border-top:0; }
|
|
414
|
-
.vrow.mine { background:var(--accent-soft); }
|
|
415
|
-
.vrow .play { min-width:78px; font:500 12.5px/1 var(--mono); }
|
|
416
|
-
.vrow .play.on { box-shadow:0 0 0 1px var(--accent); color:var(--accent); background:var(--accent-soft); }
|
|
417
|
-
.vrow .vname { min-width:0; }
|
|
418
|
-
.vrow .vname b { display:block; font-size:14.5px; font-weight:600; }
|
|
419
|
-
.vrow .vname span { display:block; font:400 11.5px/1.5 var(--mono); color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
420
|
-
.vrow .vmeta, .vrow .vtrait, .vrow .vscene { font-size:13px; color:var(--muted); }
|
|
421
|
-
.vrow .vtrait { color:var(--ink); }
|
|
422
|
-
.vrow .tags { display:flex; gap:6px; flex-wrap:wrap; }
|
|
423
|
-
.vrow .pick { font-size:12.5px; height:30px; padding:0 10px; }
|
|
424
404
|
|
|
425
405
|
/* ---------- activity:列表(每位老师先是新话题 = 老师文件)/ 原文 / 检查·预览·和出厂比 ---------- */
|
|
426
406
|
.act-wrap { max-width:none; }
|
|
@@ -531,7 +511,9 @@ ${BRAND_CSS}
|
|
|
531
511
|
@media (max-width:760px) {
|
|
532
512
|
body { grid-template-columns:1fr; grid-template-rows:auto minmax(0,1fr); }
|
|
533
513
|
#side { flex-direction:row; align-items:center; padding:10px 12px; gap:10px; overflow-x:auto; overflow-y:hidden; border-bottom:1px solid var(--line); }
|
|
534
|
-
#side .ws, #side .health, #side nav .grp, #side nav hr, #side .brand small { display:none; }
|
|
514
|
+
#side .ws, #side .health, #side nav .grp, #side nav hr, #side .brand small, #side .star b, #side .star span, #side .star .out { display:none; }
|
|
515
|
+
#side .brand-w { flex-direction:row; align-items:center; flex:none; }
|
|
516
|
+
#side .star { padding:8px; margin:0; }
|
|
535
517
|
#side nav { flex-direction:row; }
|
|
536
518
|
#side nav a { height:34px; padding:0 10px; }
|
|
537
519
|
main.on { height:auto; }
|
|
@@ -546,19 +528,16 @@ ${BRAND_CSS}
|
|
|
546
528
|
.rl { grid-template-columns:34px minmax(0,1fr); }
|
|
547
529
|
.rl .a { display:none; }
|
|
548
530
|
.pathrow { grid-template-columns:1fr; gap:6px; }
|
|
549
|
-
.vrow { grid-template-columns:auto minmax(0,1fr) auto; row-gap:4px; }
|
|
550
|
-
.vrow .vmeta, .vrow .vscene { display:none; }
|
|
551
|
-
.vrow .vtrait { grid-column:2; }
|
|
552
|
-
.vrow .tags { grid-column:2; }
|
|
553
|
-
.voices .vlist { max-height:none; }
|
|
554
531
|
#composer .row .hint { display:none; }
|
|
555
532
|
.act { grid-template-columns:1fr; }
|
|
556
533
|
.alist { position:static; }
|
|
557
|
-
.voices header .sub .cmd { display:none; }
|
|
558
534
|
}
|
|
559
535
|
</style>
|
|
560
536
|
<aside id="side">
|
|
561
|
-
<
|
|
537
|
+
<div class="brand-w" data-cotutor-brand>
|
|
538
|
+
<a class="brand ct-hover" href="#overview" data-tab="overview" title="回概览"><span class="logo ct-draw">${brandLogo({ size: 18, outer: '#fff', innerOpacity: 0.75 })}</span><b>${BRAND_NAME}</b><small>工作台</small></a>
|
|
539
|
+
<a class="star" href="${BRAND_URL}" target="_blank" rel="noopener" title="cotutor 的源码在 GitHub">${GITHUB_MARK}<b>GitHub</b><span>好用就点个 ★</span><i class="out">↗</i></a>
|
|
540
|
+
</div>
|
|
562
541
|
<div class="ws"><span class="ini" id="ws-ini">·</span><span><b id="title">cotutor</b><small id="ws-root"></small></span></div>
|
|
563
542
|
<nav>
|
|
564
543
|
<a href="#overview" data-tab="overview"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 10.5 12 3.5l8.5 7V20a1 1 0 0 1-1 1h-4.5v-6h-6v6H4.5a1 1 0 0 1-1-1z"/></svg>概览</a>
|
|
@@ -566,9 +545,7 @@ ${BRAND_CSS}
|
|
|
566
545
|
<span class="grp">老师</span>
|
|
567
546
|
<a href="#team" data-tab="team"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.6 3.4-5.5 6.5-5.5s5.7 1.9 6.5 5.5"/><path d="M16 4.6a3.5 3.5 0 0 1 0 6.8M18 14.8c1.8.7 3 2.4 3.5 5.2"/></svg>老师团</a>
|
|
568
547
|
<a href="#activity" data-tab="activity" title="孩子从首页一个按钮进去的一种做法:看、改、检查、看老师收到什么"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3.5h8.5L19 8v12.5H6z"/><path d="M14 3.5V8h5M9.5 12.5h6M9.5 16h4"/></svg>activity<span class="badge err" id="act-bad" hidden></span></a>
|
|
569
|
-
<a href="#voices" data-tab="voices"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12h1.5M7 8.5v7M11 5v14M15 8v8M19 10.5v3"/></svg>音色</a>
|
|
570
548
|
<span class="grp">工具</span>
|
|
571
|
-
<a href="/dev/qr" target="_blank" title="iPad / iPhone 用相机扫,不用输地址"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="6.5" height="6.5" rx="1"/><rect x="14" y="3.5" width="6.5" height="6.5" rx="1"/><rect x="3.5" y="14" width="6.5" height="6.5" rx="1"/><path d="M14 14h3v3h-3zM20.5 14v.5M17 20.5h3.5V17"/></svg>扫码打开<span class="out">↗</span></a>
|
|
572
549
|
<a href="/dev/voice" target="_blank" title="老师页那颗「头像 + 按住说话」的每一种样子(声音是模拟的);调动画时看这一页"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="12" r="4.5"/><path d="M15 9.5a4 4 0 0 1 0 5M18 7a7.5 7.5 0 0 1 0 10"/></svg>按钮样张<span class="out">↗</span></a>
|
|
573
550
|
<span class="grp">去看</span>
|
|
574
551
|
<a href="/" target="_blank" title="孩子端:孩子在 iPad 上看到的"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="3" width="16" height="18" rx="2.5"/><path d="M10.5 18h3"/></svg>孩子端<span class="out">↗</span></a>
|
|
@@ -606,7 +583,6 @@ ${BRAND_CSS}
|
|
|
606
583
|
</main>
|
|
607
584
|
<main id="team"></main>
|
|
608
585
|
<main id="activity"></main>
|
|
609
|
-
<main id="voices"></main>
|
|
610
586
|
<main id="settings"></main>
|
|
611
587
|
<div id="drawer"></div>
|
|
612
588
|
<script>
|
|
@@ -625,7 +601,7 @@ ${pageDomJs(4000)}
|
|
|
625
601
|
a.remove();
|
|
626
602
|
};
|
|
627
603
|
|
|
628
|
-
const TABS = ['overview', 'chat', 'team', 'activity', '
|
|
604
|
+
const TABS = ['overview', 'chat', 'team', 'activity', 'settings'];
|
|
629
605
|
const hashState = () => {
|
|
630
606
|
// #chat?raw=<job> 开某一轮的看原文;#activity?a=<老师>/<名> 开某个 activity
|
|
631
607
|
const parts = location.hash.slice(1).split('?');
|
|
@@ -643,9 +619,7 @@ ${pageDomJs(4000)}
|
|
|
643
619
|
if (tab === 'overview') renderOverview();
|
|
644
620
|
if (tab === 'activity') renderActivities();
|
|
645
621
|
if (tab === 'team') renderTeam();
|
|
646
|
-
if (tab === 'voices') renderVoices();
|
|
647
622
|
if (tab === 'settings') renderSettings();
|
|
648
|
-
if (tab !== 'voices') stopPreview();
|
|
649
623
|
};
|
|
650
624
|
for (const a of document.querySelectorAll('#side a[data-tab]')) a.addEventListener('click', (e) => { e.preventDefault(); location.hash = '#' + a.dataset.tab; showTab(a.dataset.tab); });
|
|
651
625
|
|
|
@@ -1032,7 +1006,7 @@ ${pageDomJs(4000)}
|
|
|
1032
1006
|
return h('div', { class: 'dsec' },
|
|
1033
1007
|
h('h5', {}, '发给老师的(消息正文在最后)'),
|
|
1034
1008
|
h('pre', {}, raw.pack.prompt),
|
|
1035
|
-
h('h5', {}, '命令行 · ' + raw.pack.runtime + (raw.pack.resume ? ' · resume ' + (raw.pack.session || '') : ' · 新开会话')
|
|
1009
|
+
h('h5', {}, '命令行 · ' + raw.pack.runtime + (raw.pack.resume ? ' · resume ' + (raw.pack.session || '') : ' · 新开会话')),
|
|
1036
1010
|
h('pre', {}, cmd),
|
|
1037
1011
|
raw.pack.sources ? h('p', { class: 'hintline' }, '当时的老师文件 ' + (raw.pack.sources.agent ? raw.pack.sources.agent.file + ' ' + raw.pack.sources.agent.hash.slice(7) : '(读不到)') + ' · 技能 ' + Object.entries(raw.pack.sources.skills).map(([k, v]) => k + ' ' + v.slice(7, 13)).join(' / ') + '(正文在 run.json 的 sources 里;cotutor show --json 吐)') : h('p', { class: 'hintline' }, '这轮没记老师文件与技能的快照(2026-09-15 之前跑的)'),
|
|
1038
1012
|
h('div', {},
|
|
@@ -1249,8 +1223,6 @@ ${pageDomJs(4000)}
|
|
|
1249
1223
|
h('div', { class: 'field' }, h('label', {}, '老师名(英文键)'), h('input', { type: 'text', id: 'n-name', placeholder: 'science-tutor' })),
|
|
1250
1224
|
h('div', { class: 'field' }, h('label', {}, '显示名'), h('input', { type: 'text', id: 'n-display', placeholder: '科学老师' })),
|
|
1251
1225
|
h('div', { class: 'field' }, h('label', {}, '学科(可空)'), h('input', { type: 'text', id: 'n-subject', placeholder: '科学' })),
|
|
1252
|
-
h('div', { class: 'field' }, h('label', {}, '头像(emoji,或图片路径)'), h('input', { type: 'text', id: 'n-avatar', value: '🔬', list: 'n-avatar-list', placeholder: '🔬 或 avatars/science-tutor.png' }),
|
|
1253
|
-
h('datalist', { id: 'n-avatar-list' }, ...['🧮', '📚', '📖', '🔬', '🎨', '🎵', '🌍', '💻', '🏃', '🧩', '📷', '🗓'].map((a) => h('option', { value: a })))),
|
|
1254
1226
|
h('div', { class: 'field chk' }, h('input', { type: 'checkbox', id: 'n-hidden' }), h('label', { for: 'n-hidden' }, '孩子端不露')),
|
|
1255
1227
|
h('div', { class: 'full' },
|
|
1256
1228
|
h('button', { class: 'btn primary', type: 'button', on: { click: async () => {
|
|
@@ -1258,7 +1230,7 @@ ${pageDomJs(4000)}
|
|
|
1258
1230
|
const display = $('#n-display').value.trim();
|
|
1259
1231
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return feedback(roster, false, '老师名要小写字母数字连字符');
|
|
1260
1232
|
if (!display) return feedback(roster, false, '显示名不能空');
|
|
1261
|
-
try { await api('POST', '/api/tutors', { name, display, subject: $('#n-subject').value.trim(),
|
|
1233
|
+
try { await api('POST', '/api/tutors', { name, display, subject: $('#n-subject').value.trim(), hidden: $('#n-hidden').checked }); await loadConfig(); renderTeam(); }
|
|
1262
1234
|
catch (e) { feedback(roster, false, e.message); }
|
|
1263
1235
|
} } }, '加进来'),
|
|
1264
1236
|
h('span', { class: 'hint', style: 'padding:0' }, '会写一份带全部约定的老师文件、进 cotutor.json、建目录;之后点开这位老师改「老师文件」把性子填上。与终端 cotutor add 同一条路。')));
|
|
@@ -1275,9 +1247,7 @@ ${pageDomJs(4000)}
|
|
|
1275
1247
|
h('div', { class: 'field' }, h('label', {}, '显示名'), h('input', { type: 'text', 'data-f': 'display', value: t.display })),
|
|
1276
1248
|
h('div', { class: 'field' }, h('label', {}, '学科'), h('input', { type: 'text', 'data-f': 'subject', value: t.subject || '' })),
|
|
1277
1249
|
colorField(t, c),
|
|
1278
|
-
h('div', { class: 'field' }, h('label', {}, '
|
|
1279
|
-
h('div', { class: 'field' }, h('label', {}, '卡的配音音色'), h('div', { class: 'withbtn' }, h('input', { type: 'text', 'data-f': 'voice', value: t.voice || '' }),
|
|
1280
|
-
h('button', { class: 'btn', type: 'button', title: '到音色页听着挑', on: { click: () => { voiceState.for = t.name; voiceState.kind = 'tts'; location.hash = '#voices'; showTab('voices'); } } }, '去挑'))),
|
|
1250
|
+
h('div', { class: 'field' }, h('label', {}, '头像与声音'), h('a', { href: '/parent/home', target: '_blank', style: 'line-height:36px' }, '在家长端首页「样子与声音」改 ↗')),
|
|
1281
1251
|
h('div', { class: 'field chk' }, h('input', { type: 'checkbox', 'data-f': 'enabled', checked: t.enabled }), h('label', {}, '开启')),
|
|
1282
1252
|
h('div', { class: 'field chk' }, h('input', { type: 'checkbox', 'data-f': 'hidden', checked: t.hidden }), h('label', {}, '孩子端不露')),
|
|
1283
1253
|
...policyFields(patch, t.policy, { inherit: true }));
|
|
@@ -1293,7 +1263,7 @@ ${pageDomJs(4000)}
|
|
|
1293
1263
|
saveBtn.addEventListener('click', () => {
|
|
1294
1264
|
try {
|
|
1295
1265
|
const f = (name) => $('[data-f=' + name + ']', body);
|
|
1296
|
-
patchAndReload(card, { tutors: { [t.name]: { display: f('display').value.trim(), subject: f('subject').value.trim() || null, color: f('color').dataset.v || null,
|
|
1266
|
+
patchAndReload(card, { tutors: { [t.name]: { display: f('display').value.trim(), subject: f('subject').value.trim() || null, color: f('color').dataset.v || null, enabled: f('enabled').checked, hidden: f('hidden').checked, policy: readPolicy(body) } } }, renderTeam);
|
|
1297
1267
|
} catch (e) { feedback(card, false, e.message); }
|
|
1298
1268
|
});
|
|
1299
1269
|
const caret = h('span', { class: 'caret', 'aria-hidden': 'true' });
|
|
@@ -1314,188 +1284,6 @@ ${pageDomJs(4000)}
|
|
|
1314
1284
|
$('#team').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '老师团'), h('span', { class: 'sub' }, '按 cotutor.json 里有谁走;键以 -tutor 结尾的是有脸的老师')), top, roster));
|
|
1315
1285
|
};
|
|
1316
1286
|
|
|
1317
|
-
// ---- 音色:两样声音,页头切——卡的配音(百炼 Qwen-Audio-TTS 的 597 个,src/media/tts-voices.ts;挑中的写进 tutors.<名>.voice;点读、单词卡的声音)、
|
|
1318
|
-
// 实时的嘴(百炼 realtime 认的 56 个,挑中的写进 live.voices.<名>,见下面 renderLiveVoices)。都是先听再挑 ----
|
|
1319
|
-
const AGE_BANDS = [['', '全部年龄'], ['kid', '12 岁以下'], ['young', '13–25 岁'], ['adult', '26–40 岁'], ['senior', '41 岁以上']];
|
|
1320
|
-
const inBand = (age, band) => !band || (typeof age === 'number' && (band === 'kid' ? age <= 12 : band === 'young' ? age >= 13 && age <= 25 : band === 'adult' ? age >= 26 && age <= 40 : age >= 41));
|
|
1321
|
-
const voiceState = { kind: 'tts', list: null, error: null, sample: '', inUse: {}, q: '', gender: '', band: '', text: '', for: null, playing: null, audio: null, loading: false };
|
|
1322
|
-
/** 页头的切换:卡的配音 / 实时的嘴 */
|
|
1323
|
-
const kindSeg = () => h('span', { class: 'seg', role: 'tablist' }, ...[['tts', '卡的配音'], ['live', '实时的嘴']].map(([k, label]) => h('button', { type: 'button', role: 'tab', 'aria-selected': String(voiceState.kind === k), class: voiceState.kind === k ? 'on' : '', on: { click: () => { if (voiceState.kind === k) return; stopPreview(); voiceState.kind = k; renderVoices(); } } }, label)));
|
|
1324
|
-
const stopPreview = () => {
|
|
1325
|
-
if (voiceState.audio) { voiceState.audio.pause(); voiceState.audio = null; }
|
|
1326
|
-
const b = voiceState.playing;
|
|
1327
|
-
voiceState.playing = null;
|
|
1328
|
-
if (b) { b.textContent = '▶ 试听'; b.classList.remove('on'); b.disabled = false; }
|
|
1329
|
-
};
|
|
1330
|
-
/** 点一下合成(服务端同句同音色只合成一次)并播;再点停;换一个点就切过去。base = 卡的配音 / 实时的嘴各自的试听接口 */
|
|
1331
|
-
const preview = async (btn, voice, panel, base = '/api/tts/preview') => {
|
|
1332
|
-
if (voiceState.playing === btn) return stopPreview();
|
|
1333
|
-
stopPreview();
|
|
1334
|
-
voiceState.playing = btn;
|
|
1335
|
-
btn.textContent = '合成中…';
|
|
1336
|
-
btn.classList.add('on');
|
|
1337
|
-
try {
|
|
1338
|
-
const r = await fetch(base + '?voice=' + encodeURIComponent(voice) + (voiceState.text ? '&text=' + encodeURIComponent(voiceState.text) : ''));
|
|
1339
|
-
if (!r.ok) { const j = await r.json().catch(() => null); throw new Error((j && j.message) || (r.status + ' ' + r.statusText)); }
|
|
1340
|
-
const url = URL.createObjectURL(await r.blob());
|
|
1341
|
-
if (voiceState.playing !== btn) { URL.revokeObjectURL(url); return; }
|
|
1342
|
-
const a = new Audio(url);
|
|
1343
|
-
voiceState.audio = a;
|
|
1344
|
-
btn.textContent = '■ 停';
|
|
1345
|
-
a.addEventListener('ended', () => { URL.revokeObjectURL(url); if (voiceState.audio === a) stopPreview(); });
|
|
1346
|
-
await a.play();
|
|
1347
|
-
feedback(panel, true, '');
|
|
1348
|
-
} catch (e) {
|
|
1349
|
-
if (voiceState.playing === btn) stopPreview();
|
|
1350
|
-
feedback(panel, false, '没成:' + e.message);
|
|
1351
|
-
}
|
|
1352
|
-
};
|
|
1353
|
-
const loadVoices = async () => {
|
|
1354
|
-
voiceState.loading = true;
|
|
1355
|
-
try {
|
|
1356
|
-
const r = await api('GET', '/api/tts/voices');
|
|
1357
|
-
voiceState.list = r.voices;
|
|
1358
|
-
voiceState.error = r.error;
|
|
1359
|
-
voiceState.sample = r.sample;
|
|
1360
|
-
voiceState.inUse = r.inUse || {};
|
|
1361
|
-
} catch (e) {
|
|
1362
|
-
voiceState.list = [];
|
|
1363
|
-
voiceState.error = e.message;
|
|
1364
|
-
}
|
|
1365
|
-
voiceState.loading = false;
|
|
1366
|
-
};
|
|
1367
|
-
const voiceTutor = () => {
|
|
1368
|
-
const c = state.config;
|
|
1369
|
-
return c.tutors.find((t) => t.name === voiceState.for) || c.tutors.find((t) => t.enabled && !t.hidden) || c.tutors[0] || null;
|
|
1370
|
-
};
|
|
1371
|
-
const renderVoices = async () => {
|
|
1372
|
-
const c = state.config;
|
|
1373
|
-
if (!c) return;
|
|
1374
|
-
if (voiceState.kind === 'live') return renderLiveVoices();
|
|
1375
|
-
if (!voiceState.list && !voiceState.loading) {
|
|
1376
|
-
$('#voices').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '音色'), kindSeg()), h('div', { class: 'panel' }, h('p', { class: 'hint', style: 'padding:16px 20px' }, '在列音色……'))));
|
|
1377
|
-
await loadVoices();
|
|
1378
|
-
if (state.tab !== 'voices' || voiceState.kind !== 'tts') return;
|
|
1379
|
-
}
|
|
1380
|
-
if (voiceState.loading) return;
|
|
1381
|
-
const who = voiceTutor();
|
|
1382
|
-
if (who) voiceState.for = who.name;
|
|
1383
|
-
const panel = h('div', { class: 'panel voices' });
|
|
1384
|
-
const fb = h('span', { class: 'fb' });
|
|
1385
|
-
const forSel = h('select', { id: 'v-for', title: '挑中的音色给谁', on: { change: (e) => { voiceState.for = e.target.value; renderVoices(); } } },
|
|
1386
|
-
...c.tutors.map((t) => h('option', { value: t.name, selected: who && t.name === who.name }, t.display + (t.enabled ? '' : '(已关闭)'))));
|
|
1387
|
-
panel.append(h('header', {}, h('h4', {}, '音色'), h('span', { class: 'sub' }, (voiceState.list || []).length + ' 个', h('span', { class: 'cmd' }, '百炼 Qwen-Audio-TTS')),
|
|
1388
|
-
h('span', { class: 'right' }, fb, h('label', { class: 'for' }, '给谁挑', forSel))));
|
|
1389
|
-
if (who) {
|
|
1390
|
-
const cur = (voiceState.list || []).find((v) => v.voice === who.voice);
|
|
1391
|
-
panel.append(h('div', { class: 'now' }, avatarEl(who), h('b', {}, who.display),
|
|
1392
|
-
who.voice ? h('span', {}, '现在用 ', h('b', {}, cur ? cur.name : who.voice), cur ? h('small', {}, ' ' + who.voice) : h('small', {}, '(列表里没有这个 id)')) : h('span', { class: 'muted' }, '还没配音色,点读、单词卡没有声音'),
|
|
1393
|
-
who.voice ? h('button', { class: 'btn', type: 'button', on: { click: () => patchAndReload(panel, { tutors: { [who.name]: { voice: null } } }, renderVoices) } }, '清掉') : null));
|
|
1394
|
-
}
|
|
1395
|
-
if (voiceState.error) panel.append(h('p', { class: 'fb bad', style: 'padding:12px 16px 0' }, '列不出音色:' + voiceState.error));
|
|
1396
|
-
|
|
1397
|
-
const genders = [...new Set((voiceState.list || []).map((v) => v.gender).filter(Boolean))];
|
|
1398
|
-
const filters = h('div', { class: 'filters' },
|
|
1399
|
-
h('input', { type: 'search', placeholder: '搜名字 / 特质 / 场景,比如 少年、温柔、故事', value: voiceState.q, on: { input: (e) => { voiceState.q = e.target.value.trim().toLowerCase(); renderVoiceList(); } } }),
|
|
1400
|
-
genders.length ? h('select', { on: { change: (e) => { voiceState.gender = e.target.value; renderVoiceList(); } } }, h('option', { value: '', selected: !voiceState.gender }, '全部性别'), ...genders.map((g) => h('option', { value: g, selected: voiceState.gender === g }, g))) : null,
|
|
1401
|
-
h('select', { on: { change: (e) => { voiceState.band = e.target.value; renderVoiceList(); } } }, ...AGE_BANDS.map(([k, label]) => h('option', { value: k, selected: voiceState.band === k }, label))),
|
|
1402
|
-
h('input', { type: 'text', class: 'sample', placeholder: '试听句:' + voiceState.sample, value: voiceState.text, title: '空 = 用缺省那句;每句每个音色只合成一次', on: { change: (e) => { voiceState.text = e.target.value.trim().slice(0, 200); stopPreview(); } } }));
|
|
1403
|
-
const list = h('div', { class: 'vlist' });
|
|
1404
|
-
const renderVoiceList = () => {
|
|
1405
|
-
const rows = [];
|
|
1406
|
-
const q = voiceState.q;
|
|
1407
|
-
for (const v of voiceState.list || []) {
|
|
1408
|
-
if (voiceState.gender && v.gender !== voiceState.gender) continue;
|
|
1409
|
-
if (!inBand(v.age, voiceState.band)) continue;
|
|
1410
|
-
if (q && ![v.name, v.voice, v.trait, v.scene, v.lang].some((x) => x && x.toLowerCase().includes(q))) continue;
|
|
1411
|
-
const mine = who && who.voice === v.voice;
|
|
1412
|
-
const users = (voiceState.inUse[v.voice] || []).map((n) => (c.tutors.find((t) => t.name === n) || { display: n }).display);
|
|
1413
|
-
const play = h('button', { class: 'btn play', type: 'button', on: { click: () => preview(play, v.voice, panel) } }, '▶ 试听');
|
|
1414
|
-
rows.push(h('div', { class: 'vrow' + (mine ? ' mine' : '') }, play,
|
|
1415
|
-
h('span', { class: 'vname' }, h('b', {}, v.name), h('span', {}, v.voice)),
|
|
1416
|
-
h('span', { class: 'vmeta' }, [v.gender, typeof v.age === 'number' ? v.age + ' 岁' : null].filter(Boolean).join(' · ')),
|
|
1417
|
-
h('span', { class: 'vtrait' }, v.trait || ''),
|
|
1418
|
-
h('span', { class: 'vscene' }, v.scene || ''),
|
|
1419
|
-
h('span', { class: 'tags' }, ...users.map((d) => h('span', { class: 'tag' + (mine ? ' tuned' : '') }, d + ' 在用'))),
|
|
1420
|
-
who ? (mine ? h('span', { class: 'tag tuned' }, '就是这个') : h('button', { class: 'btn primary pick', type: 'button', on: { click: () => { stopPreview(); patchAndReload(panel, { tutors: { [who.name]: { voice: v.voice } } }, renderVoices); } } }, '给' + who.display + '用')) : null));
|
|
1421
|
-
}
|
|
1422
|
-
list.replaceChildren(...(rows.length ? rows : [h('p', { class: 'hint', style: 'padding:14px 16px' }, voiceState.list && voiceState.list.length ? '没有匹配的音色,换个词' : '没有音色')]));
|
|
1423
|
-
};
|
|
1424
|
-
renderVoiceList();
|
|
1425
|
-
panel.append(filters, list);
|
|
1426
|
-
panel.append(h('p', { class: 'hint' }, '试听是真合成一次(合成过的存着,同一句同一个音色以后不再花钱;要百炼的 key,家长端「首页」里填);挑中就写进 cotutor.json,卡点了就用新声音。只管卡自己的声音(点读卡、单词卡、听写念的词);老师开口的声音在「实时的嘴」那半。'));
|
|
1427
|
-
$('#voices').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '音色'), kindSeg(), h('span', { class: 'sub' }, '先听再挑,挑中的写进 cotutor.json')), panel));
|
|
1428
|
-
};
|
|
1429
|
-
// ---- 实时的嘴:百炼 realtime 认的 56 个音色(src/realtime/voices.ts);试听是真连一次照念这一句(几分钱,存着)。
|
|
1430
|
-
// 挑中的写进 live.voices.<老师>;有脸的老师都走实时,没挑的用缺省音色;清掉 = 回到缺省 ----
|
|
1431
|
-
const liveVoiceState = { data: null, error: null, q: '', lang: '', loading: false };
|
|
1432
|
-
const loadLiveVoices = async () => {
|
|
1433
|
-
liveVoiceState.loading = true;
|
|
1434
|
-
try { liveVoiceState.data = await api('GET', '/api/tts/realtime'); liveVoiceState.error = null; } catch (e) { liveVoiceState.error = e.message; }
|
|
1435
|
-
liveVoiceState.loading = false;
|
|
1436
|
-
};
|
|
1437
|
-
const renderLiveVoices = async () => {
|
|
1438
|
-
const c = state.config;
|
|
1439
|
-
const page = (panel) => $('#voices').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '音色'), kindSeg(), h('span', { class: 'sub' }, '实时的老师开口用的声音,挑中的写进 cotutor.json 的 live.voices')), panel));
|
|
1440
|
-
if (!liveVoiceState.data && !liveVoiceState.loading) {
|
|
1441
|
-
page(h('div', { class: 'panel' }, h('p', { class: 'hint', style: 'padding:16px 20px' }, '在列音色……')));
|
|
1442
|
-
await loadLiveVoices();
|
|
1443
|
-
if (state.tab !== 'voices' || voiceState.kind !== 'live') return;
|
|
1444
|
-
}
|
|
1445
|
-
if (liveVoiceState.loading) return;
|
|
1446
|
-
const L = c.live || { voices: {} };
|
|
1447
|
-
const fallback = (liveVoiceState.data && liveVoiceState.data.fallback) || '';
|
|
1448
|
-
const list = (liveVoiceState.data && liveVoiceState.data.voices) || [];
|
|
1449
|
-
// 实时只给有脸的老师(live.voices 的键要是 -tutor 结尾的老师)
|
|
1450
|
-
const faces = c.tutors.filter((t) => t.name.endsWith('-tutor'));
|
|
1451
|
-
const who = faces.find((t) => t.name === voiceState.for) || faces.find((t) => t.enabled && !t.hidden) || faces[0] || null;
|
|
1452
|
-
if (who) voiceState.for = who.name;
|
|
1453
|
-
const panel = h('div', { class: 'panel voices' });
|
|
1454
|
-
const fb = h('span', { class: 'fb' });
|
|
1455
|
-
const forSel = h('select', { id: 'v-for', title: '挑中的音色给谁', on: { change: (e) => { voiceState.for = e.target.value; renderVoices(); } } },
|
|
1456
|
-
...faces.map((t) => h('option', { value: t.name, selected: who && t.name === who.name }, t.display + (t.enabled ? '' : '(已关闭)'))));
|
|
1457
|
-
panel.append(h('header', {}, h('h4', {}, '实时的嘴'), h('span', { class: 'sub' }, list.length + ' 个', h('span', { class: 'cmd' }, ' · ' + (L.model || ''))),
|
|
1458
|
-
h('span', { class: 'right' }, fb, h('label', { class: 'for' }, '给谁挑', forSel))));
|
|
1459
|
-
if (liveVoiceState.error) panel.append(h('p', { class: 'fb bad', style: 'padding:12px 16px 0' }, '列不出音色:' + liveVoiceState.error));
|
|
1460
|
-
if (who) {
|
|
1461
|
-
const own = L.voices[who.name];
|
|
1462
|
-
const curV = own || fallback;
|
|
1463
|
-
const cur = list.find((x) => x.voice === curV);
|
|
1464
|
-
panel.append(h('div', { class: 'now' }, avatarEl(who), h('b', {}, who.display),
|
|
1465
|
-
curV ? h('span', {}, '嘴用 ', h('b', {}, cur ? cur.name : curV), h('small', {}, ' ' + curV + (own ? '' : '(缺省的,还没挑)')), cur ? null : h('span', { class: 'fb bad' }, ' 这个模型不认这个音色,嘴会不出声,换一个')) : null,
|
|
1466
|
-
own ? h('button', { class: 'btn', type: 'button', on: { click: () => { stopPreview(); patchAndReload(panel, { live: { voices: { [who.name]: null } } }, renderVoices); } } }, '清掉(用缺省的)') : null));
|
|
1467
|
-
}
|
|
1468
|
-
const langs = [...new Set(list.map((x) => x.lang))];
|
|
1469
|
-
const filters = h('div', { class: 'filters' },
|
|
1470
|
-
h('input', { type: 'search', placeholder: '搜名字 / 特质,比如 温柔、知性、少年', value: liveVoiceState.q, on: { input: (e) => { liveVoiceState.q = e.target.value.trim().toLowerCase(); renderList(); } } }),
|
|
1471
|
-
h('select', { on: { change: (e) => { liveVoiceState.lang = e.target.value; renderList(); } } }, h('option', { value: '', selected: !liveVoiceState.lang }, '普通话与方言都列'), ...langs.map((g) => h('option', { value: g, selected: liveVoiceState.lang === g }, g))),
|
|
1472
|
-
h('input', { type: 'text', class: 'sample', placeholder: '试听句:' + ((liveVoiceState.data && liveVoiceState.data.sample) || ''), value: voiceState.text, title: '空 = 用缺省那句;每句每个音色只念一次', on: { change: (e) => { voiceState.text = e.target.value.trim().slice(0, 200); stopPreview(); } } }));
|
|
1473
|
-
const box = h('div', { class: 'vlist' });
|
|
1474
|
-
const renderList = () => {
|
|
1475
|
-
const rows = [];
|
|
1476
|
-
const q = liveVoiceState.q;
|
|
1477
|
-
for (const x of list) {
|
|
1478
|
-
if (liveVoiceState.lang && x.lang !== liveVoiceState.lang) continue;
|
|
1479
|
-
if (q && ![x.name, x.voice, x.trait, x.lang].some((y) => y && y.toLowerCase().includes(q))) continue;
|
|
1480
|
-
const mine = who && (L.voices[who.name] || fallback) === x.voice;
|
|
1481
|
-
const users = ((liveVoiceState.data && liveVoiceState.data.inUse) || {})[x.voice] ? liveVoiceState.data.inUse[x.voice].map((n) => (c.tutors.find((t) => t.name === n) || { display: n }).display) : [];
|
|
1482
|
-
const play = h('button', { class: 'btn play', type: 'button', on: { click: () => preview(play, x.voice, panel, '/api/tts/realtime/preview') } }, '▶ 试听');
|
|
1483
|
-
rows.push(h('div', { class: 'vrow' + (mine ? ' mine' : '') }, play,
|
|
1484
|
-
h('span', { class: 'vname' }, h('b', {}, x.name), h('span', {}, x.voice)),
|
|
1485
|
-
h('span', { class: 'vmeta' }, x.lang),
|
|
1486
|
-
h('span', { class: 'vtrait' }, x.trait),
|
|
1487
|
-
h('span', { class: 'vscene' }, ''),
|
|
1488
|
-
h('span', { class: 'tags' }, ...users.map((d) => h('span', { class: 'tag' + (mine ? ' tuned' : '') }, d + ' 在用'))),
|
|
1489
|
-
who ? (mine ? h('span', { class: 'tag tuned' }, '就是这个') : h('button', { class: 'btn primary pick', type: 'button', on: { click: () => { stopPreview(); patchAndReload(panel, { live: { voices: { [who.name]: x.voice } } }, renderVoices); } } }, '给' + who.display + '用')) : null));
|
|
1490
|
-
}
|
|
1491
|
-
box.replaceChildren(...(rows.length ? rows : [h('p', { class: 'hint', style: 'padding:14px 16px' }, '没有匹配的音色,换个词')]));
|
|
1492
|
-
};
|
|
1493
|
-
renderList();
|
|
1494
|
-
panel.append(filters, box);
|
|
1495
|
-
panel.append(h('p', { class: 'hint' }, '试听是真连一次百炼的实时模型照念这一句(几分钱;同一句同一个音色只念一次,存在 .cotutor/live-preview/)。挑中就写进 cotutor.json 的 live.voices,孩子下一个新话题起用。这个模型没有童声;方言、外国口音的不适合教课。'));
|
|
1496
|
-
page(panel);
|
|
1497
|
-
};
|
|
1498
|
-
|
|
1499
1287
|
// ---- 设置:路径 / 服务 / 配音;文件仍是真相 ----
|
|
1500
1288
|
const PATH_ROLES = [['vault', 'vault 根', 'Obsidian 仓库;空 = workspace 根'], ['profile', '新建档案的位置', '相对 vault;老师按 cotutor: profile 属性找档案'], ['timetable', '课程表', '相对 vault'], ['plans', '计划目录', ''], ['diary', '日记目录', '记账写这里'], ['textbooks', '教材目录', '记账列可选的册#节;上下文包按 cotutor: textbook 属性找'], ['reference', '参考目录', '你自己的笔记'], ['captures', '作业照片', '相对 workspace 根(不进 vault)']];
|
|
1501
1289
|
|
|
@@ -1532,7 +1320,7 @@ ${pageDomJs(4000)}
|
|
|
1532
1320
|
}
|
|
1533
1321
|
|
|
1534
1322
|
const server = h('div', { class: 'panel' });
|
|
1535
|
-
server.append(h('header', {}, h('h4', {}, '服务'), h('span', { class: 'sub' }, '
|
|
1323
|
+
server.append(h('header', {}, h('h4', {}, '服务'), h('span', { class: 'sub' }, '改端口要重启服务,平板要重新扫码、重加主屏幕图标'),
|
|
1536
1324
|
h('span', { class: 'right' }, h('span', { class: 'fb' }), h('button', { class: 'btn primary', type: 'button', on: { click: async () => {
|
|
1537
1325
|
try {
|
|
1538
1326
|
const port = Number($('#s-port').value);
|
|
@@ -1541,7 +1329,7 @@ ${pageDomJs(4000)}
|
|
|
1541
1329
|
const key = $('#s-key').value.trim();
|
|
1542
1330
|
await api('PATCH', '/api/config', { server: { port, https: cert && key ? { cert, key } : null } });
|
|
1543
1331
|
await loadConfig();
|
|
1544
|
-
feedback(server, true, '已写入' + (port !== c.server.port ? '
|
|
1332
|
+
feedback(server, true, '已写入' + (port !== c.server.port ? ';端口改了,重启服务才生效;平板要重新扫码、重加主屏幕图标' : ''));
|
|
1545
1333
|
} catch (e) { feedback(server, false, e.message); }
|
|
1546
1334
|
} } }, '保存服务'))));
|
|
1547
1335
|
server.append(h('div', { class: 'grid' },
|
|
@@ -1549,21 +1337,8 @@ ${pageDomJs(4000)}
|
|
|
1549
1337
|
h('div', { class: 'field' }, h('label', {}, 'HTTPS 证书(相对 workspace)'), h('input', { type: 'text', id: 's-cert', value: c.https ? c.https.cert : '', placeholder: '空 = 机器级 ~/.config/cotutor/certs/' })),
|
|
1550
1338
|
h('div', { class: 'field' }, h('label', {}, 'HTTPS 私钥'), h('input', { type: 'text', id: 's-key', value: c.https ? c.https.key : '', placeholder: '空 = 机器级' }))));
|
|
1551
1339
|
|
|
1552
|
-
// 孩子端:惯用手(kid.hand)——左撇子时平板横屏实时老师页的浮层(按住说话)挪到右边;孩子端下次进老师页生效
|
|
1553
|
-
const kid = h('div', { class: 'panel' });
|
|
1554
|
-
const left = (c.kid && c.kid.hand) === 'left';
|
|
1555
|
-
const handBtn = (v, label) => h('button', { type: 'button', class: left === (v === 'left') ? 'on' : '', 'aria-pressed': String(left === (v === 'left')), on: { click: async () => {
|
|
1556
|
-
if (left === (v === 'left')) return;
|
|
1557
|
-
try { await api('PATCH', '/api/config', { kid: { hand: v === 'left' ? 'left' : null } }); await loadConfig(); renderSettings(); }
|
|
1558
|
-
catch (e) { feedback(kid, false, e.message); }
|
|
1559
|
-
} } }, label);
|
|
1560
|
-
kid.append(h('header', {}, h('h4', {}, '孩子端'), h('span', { class: 'sub' }, 'kid.hand · 孩子下次进老师页生效'), h('span', { class: 'right' }, h('span', { class: 'fb' }))),
|
|
1561
|
-
h('div', { class: 'handrow' }, h('b', {}, '惯用手'), h('span', { class: 'seg' }, handBtn('right', '右手'), handBtn('left', '左撇子')),
|
|
1562
|
-
h('span', { class: 'muted' }, left ? '平板横屏的实时老师页:按住说话那一栏在右边,左手写字' : '平板横屏的实时老师页:按住说话那一栏在左边,右手写字')),
|
|
1563
|
-
h('p', { class: 'hint' }, 'kid 的别的字段、version、运行时模板请直接编辑 cotutor.json。'));
|
|
1564
|
-
|
|
1565
1340
|
const mig = migratePanel();
|
|
1566
|
-
$('#settings').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '设置'), h('span', { class: 'sub' }, '
|
|
1341
|
+
$('#settings').replaceChildren(h('div', { class: 'wrap' }, h('header', { class: 'ph' }, h('h1', {}, '设置'), h('span', { class: 'sub' }, '路径、服务;文件仍是真相,这页只是改它的一个口子')), ...(mig ? [mig] : []), paths, server));
|
|
1567
1342
|
};
|
|
1568
1343
|
|
|
1569
1344
|
// ---- 概览:近七天的数字、今天各老师、要看一眼的轮次、体检(doctor 不带 --live) ----
|
|
@@ -1614,8 +1389,6 @@ ${pageDomJs(4000)}
|
|
|
1614
1389
|
h('button', { type: 'button', on: { click: () => { location.hash = '#chat'; showTab('chat'); $('#text').focus(); } } }, icon('<path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H9l-5 4z"/><path d="M8 9h8M8 12h5"/>', 'hot'), h('b', {}, '发一句试试'), h('small', {}, '以孩子的身份跑一轮')),
|
|
1615
1390
|
h('button', { type: 'button', disabled: !d.last, on: { click: () => d.last && goChat(d.last.tutor, d.last.date, d.last.job) } }, icon('<path d="M8 7 3 12l5 5M16 7l5 5-5 5M13.5 4.5l-3 15"/>'), h('b', {}, '看原文'), h('small', { class: 'num' }, d.last ? '最近一轮 ' + (d.last.date === d.today ? '' : d.last.date.slice(5) + ' ') + d.last.at.slice(11, 16) : '七天里没有')),
|
|
1616
1391
|
h('button', { type: 'button', on: { click: () => { location.hash = '#team'; showTab('team'); } } }, icon('<circle cx="9" cy="8" r="3.5"/><path d="M2.5 20c.8-3.6 3.4-5.5 6.5-5.5s5.7 1.9 6.5 5.5"/><path d="M16 4.6a3.5 3.5 0 0 1 0 6.8M18 14.8c1.8.7 3 2.4 3.5 5.2"/>'), h('b', {}, '老师团'), h('small', {}, (c ? c.tutors.filter((t) => t.enabled).length : 0) + ' 位开着')),
|
|
1617
|
-
h('button', { type: 'button', on: { click: () => { location.hash = '#voices'; showTab('voices'); } } }, icon('<path d="M3 12h1.5M7 8.5v7M11 5v14M15 8v8M19 10.5v3"/>'), h('b', {}, '音色'), h('small', {}, '试听、换声')),
|
|
1618
|
-
h('a', { href: '/dev/qr', target: '_blank' }, icon('<rect x="3.5" y="3.5" width="6.5" height="6.5" rx="1"/><rect x="14" y="3.5" width="6.5" height="6.5" rx="1"/><rect x="3.5" y="14" width="6.5" height="6.5" rx="1"/><path d="M14 14h3v3h-3zM20.5 14v.5M17 20.5h3.5V17"/>'), h('b', {}, '扫码打开'), h('small', {}, 'iPad · iPhone')),
|
|
1619
1392
|
h('button', { type: 'button', disabled: ov.doctorBusy, on: { click: runDoctor } }, icon('<path d="M3 12h4l2.5-6 5 12 2.5-6h4"/>'), h('b', {}, '体检一次'), h('small', { class: 'num' }, ov.doctorBusy ? '在跑……' : 'doctor')));
|
|
1620
1393
|
|
|
1621
1394
|
const slowEl = d.slow ? h('span', { class: 'st warn' }, d.slow + ' 轮') : h('span', { class: 'st ok' }, '没有');
|
|
@@ -2087,7 +1860,7 @@ ${pageDomJs(4000)}
|
|
|
2087
1860
|
...Object.entries(pv.values).map(([k, v]) => h('span', { class: 'tag' }, k + ' ' + v)),
|
|
2088
1861
|
...pv.paramIssues.map((x) => h('span', { class: 'st err' }, x))));
|
|
2089
1862
|
out.push(h('pre', { class: 'pv' }, pv.text));
|
|
2090
|
-
out.push(h('div', { class: 'srow' }, h('span', { class: 'note', style: 'flex:1;min-width:160px' }, '
|
|
1863
|
+
out.push(h('div', { class: 'srow' }, h('span', { class: 'note', style: 'flex:1;min-width:160px' }, '这就是这个话题老师的系统提示:不读老师文件,注释已剥,后面再接这几种卡的板书写法。'), h('button', { class: 'btn small', type: 'button', on: { click: (e) => copy(pv.text, e.target) } }, '复制全文')));
|
|
2091
1864
|
}
|
|
2092
1865
|
for (const t of pv.tools) {
|
|
2093
1866
|
out.push(rule(), h('h3', {}, t.tool + '收到什么'), h('p', { class: 'note' }, '{{插槽}} 由应用在交出去那一刻填。'), h('pre', { class: 'pv tool' }, t.prompt));
|
|
@@ -2144,7 +1917,7 @@ ${pageDomJs(4000)}
|
|
|
2144
1917
|
pv.board ? h('span', { class: 'tag', title: pv.board.cards.length ? '这位老师的卡:' + pv.board.cards.join(' ') : '全部的卡' }, '+ 板书写法 ' + pv.board.chars.toLocaleString() + ' 字') : null,
|
|
2145
1918
|
pv.rules ? h('span', { class: 'tag' }, '+ 守则 ' + pv.rules.chars.toLocaleString() + ' 字') : null)];
|
|
2146
1919
|
out.push(h('pre', { class: 'pv' }, pv.text || '(空)'));
|
|
2147
|
-
out.push(h('div', { class: 'srow' }, h('span', { class: 'note', style: 'flex:1;min-width:160px' }, '
|
|
1920
|
+
out.push(h('div', { class: 'srow' }, h('span', { class: 'note', style: 'flex:1;min-width:160px' }, '这就是新话题里老师的系统提示:老师文件去掉 frontmatter' + (pv.board ? ',后面再接板书写法' : '') + '。'), h('button', { class: 'btn small', type: 'button', on: { click: (e) => copy(pv.text, e.target) } }, '复制全文')));
|
|
2148
1921
|
if (pv.rules) {
|
|
2149
1922
|
out.push(rule(), h('h3', {}, '话题第一条还带守则'),
|
|
2150
1923
|
h('p', { class: 'note' }, '有脸的老师共用的 ' + pv.rules.path + ',机器件,cotutor upgrade 每次覆盖,这里不改。这位老师要不一样的做法,写进上面的老师文件,它优先。'),
|
package/dist/ui/dev/dev-theme.js
CHANGED
|
@@ -43,7 +43,7 @@ __VOICE_CSS__
|
|
|
43
43
|
<header>
|
|
44
44
|
<h1>按钮样张</h1>
|
|
45
45
|
<p>孩子端老师页那颗「头像 + 按住说话」的每一种样子,声音是模拟的。要改动画:改 src/ui/kid/voice-ui.ts 开头的调参表;样子的种类在 src/ui/kid/voice-view.ts,场景表在 src/ui/dev/voice-scenes.ts。</p>
|
|
46
|
-
<label class="sw"><input type="checkbox" id="mirror"
|
|
46
|
+
<label class="sw"><input type="checkbox" id="mirror">按住说话在右边</label>
|
|
47
47
|
</header>
|
|
48
48
|
<main id="main"></main>
|
|
49
49
|
<script>
|
|
@@ -4,7 +4,7 @@ export type BoardMarkId = keyof typeof BOARD_MARK_ART;
|
|
|
4
4
|
export declare function boardMarkOf(subject: string | null | undefined): BoardMarkId | null;
|
|
5
5
|
/**
|
|
6
6
|
* 怎么摆:right = 外框右沿离板右沿多远(乘 s;负数出纸边),top = 离板顶多少 px(加安全区),o = 不透明度。
|
|
7
|
-
* top 从两角按钮下面起(实时页回首页胶囊下沿 64、「更多」54,再空出十几 px)
|
|
7
|
+
* top 从两角按钮下面起(实时页回首页胶囊下沿 64、「更多」54,再空出十几 px),那一栏在左边时不让「更多」压着、在右边时不让胶囊压着;
|
|
8
8
|
* 三种字墨量差得多(粗宋最重、ENGLISH 最轻),浓淡分开压到看着一样淡;ENGLISH 是孩子认得的词,字母不切边
|
|
9
9
|
*/
|
|
10
10
|
export declare const BOARD_MARK_PLACE: Readonly<Record<BoardMarkId, {
|
|
@@ -18,7 +18,7 @@ export declare const BOARD_MARK_BOTTOM = 16;
|
|
|
18
18
|
export declare const BOARD_INK = "#4a3f2e";
|
|
19
19
|
/** 老师页的样式:字在 #bmark */
|
|
20
20
|
export declare const BOARD_MARK_CSS = "\n #bmark { position:absolute; inset:0; z-index:-1; overflow:hidden; pointer-events:none; container-type:size; color:#4a3f2e; }\n #bmark[hidden] { display:none; }\n #bmark svg { position:absolute; display:block; --s:min(48vh, 72vw, (100vh - env(safe-area-inset-top) - var(--bm-top) * 1px - 16px) / var(--bm-h)); top:calc(env(safe-area-inset-top) + var(--bm-top) * 1px); right:calc(var(--s) * var(--bm-right)); width:calc(var(--s) * var(--bm-w)); height:calc(var(--s) * var(--bm-h)); opacity:var(--bm-o); }\n @supports (width:1cqh) { #bmark svg { --s:min(48cqh, 72cqw, (100cqh - env(safe-area-inset-top) - var(--bm-top) * 1px - 16px) / var(--bm-h)); } }\n";
|
|
21
|
-
/**
|
|
21
|
+
/** 平板横屏实时、按住说话在右边(kid.hand: left):白纸在右边,字挪到左边(插进老师页那段横屏媒体查询里) */
|
|
22
22
|
export declare const BOARD_MARK_HAND_LEFT_CSS = "body.live.hand-left #bmark svg { right:auto; left:calc(var(--s) * var(--bm-right)); }";
|
|
23
23
|
/** markBoard(el, t):把 t.mark 那个字放进 el(#bmark);t 没有 mark(别的老师、还不知道是谁)就收起来 */
|
|
24
24
|
export declare const BOARD_MARK_JS: string;
|
|
@@ -14,7 +14,7 @@ export function boardMarkOf(subject) {
|
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* 怎么摆:right = 外框右沿离板右沿多远(乘 s;负数出纸边),top = 离板顶多少 px(加安全区),o = 不透明度。
|
|
17
|
-
* top 从两角按钮下面起(实时页回首页胶囊下沿 64、「更多」54,再空出十几 px)
|
|
17
|
+
* top 从两角按钮下面起(实时页回首页胶囊下沿 64、「更多」54,再空出十几 px),那一栏在左边时不让「更多」压着、在右边时不让胶囊压着;
|
|
18
18
|
* 三种字墨量差得多(粗宋最重、ENGLISH 最轻),浓淡分开压到看着一样淡;ENGLISH 是孩子认得的词,字母不切边
|
|
19
19
|
*/
|
|
20
20
|
export const BOARD_MARK_PLACE = {
|
|
@@ -33,7 +33,7 @@ export const BOARD_MARK_CSS = `
|
|
|
33
33
|
#bmark svg { position:absolute; display:block; --s:min(48vh, 72vw, (100vh - env(safe-area-inset-top) - var(--bm-top) * 1px - ${BOARD_MARK_BOTTOM}px) / var(--bm-h)); top:calc(env(safe-area-inset-top) + var(--bm-top) * 1px); right:calc(var(--s) * var(--bm-right)); width:calc(var(--s) * var(--bm-w)); height:calc(var(--s) * var(--bm-h)); opacity:var(--bm-o); }
|
|
34
34
|
@supports (width:1cqh) { #bmark svg { --s:min(48cqh, 72cqw, (100cqh - env(safe-area-inset-top) - var(--bm-top) * 1px - ${BOARD_MARK_BOTTOM}px) / var(--bm-h)); } }
|
|
35
35
|
`;
|
|
36
|
-
/**
|
|
36
|
+
/** 平板横屏实时、按住说话在右边(kid.hand: left):白纸在右边,字挪到左边(插进老师页那段横屏媒体查询里) */
|
|
37
37
|
export const BOARD_MARK_HAND_LEFT_CSS = `body.live.hand-left #bmark svg { right:auto; left:calc(var(--s) * var(--bm-right)); }`;
|
|
38
38
|
const MARKS_JSON = JSON.stringify(Object.fromEntries(Object.keys(BOARD_MARK_ART).map((id) => {
|
|
39
39
|
const a = BOARD_MARK_ART[id];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 首页老师牌上常驻的三块方砖(预习、检查作业、复习;设计稿 https://claude.ai/artifact/NpPFoyvuF17dbRSEUuRJfW):
|
|
3
|
+
* 一行三块,白底、学科色细边、图在上字在下;三科同一个位置、同一个样子,分辨全靠图——
|
|
4
|
+
* 预习 = 翻开的书 + 一颗小星星,检查作业 = 作业本 + 放大镜,复习 = 学过的一摞。学科特色只在图里(颜色只用学科色,currentColor)。
|
|
5
|
+
* 图按老师的 mark(板书底纹那个:三门主科各一套)挑,别的老师用不带学科内容的一套。数学三张都有阿拉伯数字。
|
|
6
|
+
* 方砖下面不写小字(家长填的范围不上孩子的牌)。点了和 activity 按钮一样:新开话题、发出方砖上的字;今天做过的先问接着还是重新开始。
|
|
7
|
+
*/
|
|
8
|
+
import type { HomeTileId } from '../../schema/index.ts';
|
|
9
|
+
import type { BoardMarkId } from './board-mark.ts';
|
|
10
|
+
type TileArt = Record<HomeTileId, string>;
|
|
11
|
+
export declare const TILE_ART: Readonly<Record<BoardMarkId | 'plain', TileArt>>;
|
|
12
|
+
/** 内联进孩子端页面:tileArt(mark, id) → 整个 <svg>(aria-hidden,字在旁边的 .btx 里) */
|
|
13
|
+
export declare const HOME_TILES_JS: string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 都是 viewBox 0 0 88 60;currentColor = 学科色(方砖上的 --sc),浅色线是固定的米色 / 灰蓝
|
|
2
|
+
const STAR = '<path d="M44 1 L45.6 5.4 L50 7 L45.6 8.6 L44 13 L42.4 8.6 L38 7 L42.4 5.4 Z" fill="#f2b84b"></path>';
|
|
3
|
+
const BOOK = '<path d="M44 15 C34 9 21 9 9 13 V53 C21 49 34 49 44 55 Z" fill="#fff" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path><path d="M44 15 C54 9 67 9 79 13 V53 C67 49 54 49 44 55 Z" fill="#fff" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path>';
|
|
4
|
+
const NOTEBOOK = '<rect x="8" y="6" width="58" height="48" rx="4" fill="#fff" stroke="#d9b97a" stroke-width="1.5"></rect><path d="M18 7 V2 M30 7 V2 M42 7 V2 M54 7 V2" stroke="#b9a988" stroke-width="2" stroke-linecap="round"></path>';
|
|
5
|
+
const LENS = '<circle cx="66" cy="40" r="11" fill="#ffffffd9" stroke="currentColor" stroke-width="3"></circle><path d="M74 48 L82 56" stroke="currentColor" stroke-width="4.5" stroke-linecap="round"></path>';
|
|
6
|
+
const SONG = 'font-family="Songti SC, STSong, serif"';
|
|
7
|
+
const ROUND = 'font-family="ui-rounded, -apple-system, sans-serif"';
|
|
8
|
+
const HAND = 'font-family="Chalkboard SE, Comic Sans MS, Marker Felt, sans-serif"';
|
|
9
|
+
const MONO = 'font-family="Menlo, monospace"';
|
|
10
|
+
/** 一摞里后面那张(淡的、斜着) */
|
|
11
|
+
const back = (x, y, w, h, rot, cx, cy, text, size, font, rx = 3) => `<g transform="rotate(${rot} ${cx} ${cy})"><rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${rx}" fill="#fff" stroke="#e2d6bd" stroke-width="1.5"></rect>${text ? `<text x="${x + w / 2}" y="${y + h / 2 + size * 0.36}" font-size="${size}" text-anchor="middle" fill="#b9a988" ${font}>${text}</text>` : ''}</g>`;
|
|
12
|
+
/** 田字格一格 */
|
|
13
|
+
const grid = (x, y, s, ch, size, color = '#8f836c') => `<rect x="${x}" y="${y}" width="${s}" height="${s}" fill="none" stroke="#e2d6bd"></rect>${ch ? `<text x="${x + s / 2}" y="${y + s / 2 + size * 0.38}" font-size="${size}" text-anchor="middle" fill="${color}" ${SONG}>${ch}</text>` : ''}`;
|
|
14
|
+
export const TILE_ART = {
|
|
15
|
+
yuwen: {
|
|
16
|
+
preview: `${STAR}${BOOK}<path d="M15 24 H37 M15 32 H37 M15 40 H31" stroke="#d9b97a" stroke-width="2" stroke-linecap="round"></path><rect x="52" y="21" width="21" height="21" fill="#fff" stroke="currentColor" stroke-width="1.5"></rect><path d="M62.5 21 V42 M52 31.5 H73" stroke="currentColor" stroke-opacity=".35" stroke-width="1" stroke-dasharray="2 2"></path><text x="62.5" y="37.5" font-size="16" font-weight="700" text-anchor="middle" fill="currentColor" ${SONG}>春</text>`,
|
|
17
|
+
check: `${NOTEBOOK}${grid(14, 14, 14, '天', 11)}${grid(30, 14, 14, '地', 11)}${grid(46, 14, 14, '人', 11)}${grid(14, 32, 14, '日', 11)}${grid(30, 32, 14, '月', 11)}${LENS}`,
|
|
18
|
+
review: `${back(10, 24, 27, 27, -14, 24, 38, '水', 16, SONG)}${back(45, 24, 27, 27, 12, 58, 38, '火', 16, SONG)}<rect x="25" y="16" width="32" height="32" rx="3" fill="#fff" stroke="currentColor" stroke-width="2"></rect><path d="M41 16 V48 M25 32 H57" stroke="currentColor" stroke-opacity=".3" stroke-width="1" stroke-dasharray="2 2"></path><text x="41" y="39" font-size="20" font-weight="700" text-anchor="middle" fill="currentColor" ${SONG}>山</text>`,
|
|
19
|
+
},
|
|
20
|
+
shuxue: {
|
|
21
|
+
preview: `${STAR}${BOOK}<path d="M17 21 V35 M22 21 V35 M27 21 V35" stroke="#e3a64a" stroke-width="2.6" stroke-linecap="round"></path><text x="22" y="47" font-size="11" font-weight="700" text-anchor="middle" fill="#8f836c" ${ROUND}>3</text><text x="61.5" y="31" font-size="13" font-weight="800" text-anchor="middle" fill="currentColor" ${ROUND}>3+4</text><text x="61.5" y="46" font-size="13" font-weight="800" text-anchor="middle" fill="currentColor" ${ROUND}>=?</text>`,
|
|
22
|
+
check: `${NOTEBOOK}<text x="44" y="21" font-size="11" text-anchor="end" fill="#8f836c" ${MONO}>27</text><text x="44" y="33" font-size="11" text-anchor="end" fill="#8f836c" ${MONO}>+15</text><path d="M20 37 H46" stroke="#8f836c" stroke-width="1.2"></path><text x="44" y="48" font-size="11" text-anchor="end" fill="#8f836c" ${MONO}>42</text>${LENS}`,
|
|
23
|
+
review: `${back(12, 12, 44, 28, -12, 34, 26, '6+9', 12, ROUND, 5)}${back(34, 10, 44, 28, 9, 56, 24, '15−7', 12, ROUND, 5)}<rect x="20" y="24" width="50" height="32" rx="5" fill="#fff" stroke="currentColor" stroke-width="2"></rect><text x="45" y="46" font-size="17" font-weight="800" text-anchor="middle" fill="currentColor" ${ROUND}>8×7</text>`,
|
|
24
|
+
},
|
|
25
|
+
english: {
|
|
26
|
+
preview: `${STAR}${BOOK}<path d="M14 23 H38 M14 29 H38 M14 41 H38" stroke="#cdd6e2" stroke-width="1.2"></path><path d="M14 35 H38" stroke="#e39a9a" stroke-width="1.2"></path><text x="62" y="39" font-size="18" font-weight="700" text-anchor="middle" fill="currentColor" ${HAND}>Aa</text>`,
|
|
27
|
+
check: `${NOTEBOOK}<path d="M14 14 H58 M14 19 H58 M14 29 H58 M14 34 H58 M14 39 H58 M14 49 H58" stroke="#cdd6e2" stroke-width="1"></path><path d="M14 24 H58 M14 44 H58" stroke="#e39a9a" stroke-width="1"></path><text x="16" y="24" font-size="11" fill="#8f836c" ${HAND}>apple</text><text x="16" y="44" font-size="11" fill="#8f836c" ${HAND}>pen</text>${LENS}`,
|
|
28
|
+
review: `${back(10, 14, 48, 28, -10, 36, 28, 'dog', 13, HAND, 5)}<rect x="24" y="26" width="50" height="30" rx="5" fill="#fff" stroke="currentColor" stroke-width="2"></rect><text x="49" y="47" font-size="16" font-weight="700" text-anchor="middle" fill="currentColor" ${HAND}>cat</text>`,
|
|
29
|
+
},
|
|
30
|
+
// 别的老师:同样的书、本子、一摞,不带学科内容
|
|
31
|
+
plain: {
|
|
32
|
+
preview: `${STAR}${BOOK}<path d="M15 24 H37 M15 32 H37 M15 40 H31 M51 24 H73 M51 32 H73 M51 40 H67" stroke="#d9b97a" stroke-width="2" stroke-linecap="round"></path>`,
|
|
33
|
+
check: `${NOTEBOOK}<path d="M15 18 H58 M15 28 H52 M15 38 H46" stroke="#cdbf9f" stroke-width="2" stroke-linecap="round"></path>${LENS}`,
|
|
34
|
+
review: `${back(12, 14, 44, 28, -12, 34, 28, '', 12, ROUND, 5)}${back(34, 12, 44, 28, 9, 56, 26, '', 12, ROUND, 5)}<rect x="20" y="24" width="50" height="32" rx="5" fill="#fff" stroke="currentColor" stroke-width="2"></rect><path d="M29 35 H61 M29 44 H53" stroke="currentColor" stroke-opacity=".5" stroke-width="2" stroke-linecap="round"></path>`,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
/** 内联进孩子端页面:tileArt(mark, id) → 整个 <svg>(aria-hidden,字在旁边的 .btx 里) */
|
|
38
|
+
export const HOME_TILES_JS = `
|
|
39
|
+
var TILE_ART = ${JSON.stringify(TILE_ART)};
|
|
40
|
+
var tileArt = function (mark, id) {
|
|
41
|
+
var set = TILE_ART[mark] || TILE_ART.plain;
|
|
42
|
+
return '<svg viewBox="0 0 88 60" aria-hidden="true" focusable="false">' + (set[id] || '') + '</svg>';
|
|
43
|
+
};
|
|
44
|
+
`;
|