cotutor 0.3.0 → 0.4.1
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 +65 -18
- 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 +11 -0
- package/dist/app/cli/cert.js +36 -9
- 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 +11 -5
- package/dist/app/cli/doctor.js +156 -181
- 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 +379 -27
- 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 +152 -0
- package/dist/app/cli/service.js +443 -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 +42 -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 +236 -0
- package/dist/app/cli/tray.js +670 -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 +47 -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/machine/update.d.ts +25 -0
- package/dist/app/machine/update.js +88 -0
- 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/shared/upgrade.d.ts +9 -0
- package/dist/shared/upgrade.js +23 -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 +337 -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 +6 -2
- 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
package/dist/ui/kid/kid-page.js
CHANGED
|
@@ -45,6 +45,7 @@ import { pageDomJs } from "../shared/page-dom.js";
|
|
|
45
45
|
import { KID_LOG_JS } from "./kid-log.js";
|
|
46
46
|
import { BACK_TINT_CSS, BACK_TINT_JS } from "./tutor-face.js";
|
|
47
47
|
import { BOARD_MARK_CSS, BOARD_MARK_HAND_LEFT_CSS, BOARD_MARK_JS } from "./board-mark.js";
|
|
48
|
+
import { HOME_TILES_JS } from "./home-tiles.js";
|
|
48
49
|
/** 内联进页面的那几份纯文件(仓库里是 .ts,npm 包里是 dist 的 .js;inline-lib 剥类型、去 export,变成能内联的普通脚本) */
|
|
49
50
|
const INLINE = {
|
|
50
51
|
'kid-board': new URL('../../board/presentation.ts', import.meta.url),
|
|
@@ -127,17 +128,8 @@ ${faviconLinks()}
|
|
|
127
128
|
.c-mark .mk-t small { font-size:13px; font-weight:700; color:#2f6fd6; }
|
|
128
129
|
.c-mark .mk-t b { font-size:22px; font-variant-numeric:tabular-nums; }
|
|
129
130
|
.c-mark .x { position:absolute; right:4px; top:4px; width:40px; height:40px; border-radius:50%; border:none; background:transparent; font-size:22px; line-height:1; color:var(--dim); }
|
|
130
|
-
/*
|
|
131
|
+
/* 卡下沿的一条(拍给我看卡的「等你拍…」):孩子的话不在卡上,在左栏 */
|
|
131
132
|
.kid-ans { margin:8px -16px -14px; padding:10px 16px 12px; border:0; border-top:1.5px dashed var(--tint-paper-line); border-radius:0 0 14px 14px; background:transparent; display:flex; align-items:center; gap:9px; font:inherit; font-size:18px; font-weight:500; line-height:1.5; color:var(--ink); text-align:left; cursor:pointer; }
|
|
132
|
-
.kid-ans.wait { display:none; font-weight:400; color:var(--dim); cursor:default; }
|
|
133
|
-
body.waiting .kid-ans.wait { display:flex; }
|
|
134
|
-
.sh.said { gap:10px; padding-top:14px; }
|
|
135
|
-
.sh.said::after { display:none; }
|
|
136
|
-
.sh.said .ln { flex:1 1 12px; min-width:12px; height:1px; margin-left:6px; background:var(--line); }
|
|
137
|
-
.sh.said .ka-t { font-size:22px; font-weight:600; letter-spacing:.5px; line-height:1.4; color:var(--ink); cursor:pointer; }
|
|
138
|
-
.kid-ans .ka-t, .sh.said .ka-t { flex:0 1 auto; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
139
|
-
.kid-ans .ka-t { flex-grow:1; }
|
|
140
|
-
.kid-ans.open .ka-t, .sh.said.open .ka-t { white-space:normal; }
|
|
141
133
|
.lc-still { width:100%; height:100%; }
|
|
142
134
|
.lc-still img { width:100%; height:100%; display:block; object-fit:contain; }
|
|
143
135
|
#rest { display:none; text-align:center; color:var(--dim); font-size:16px; padding:12px 0; }
|
|
@@ -275,7 +267,6 @@ ${BACK_TINT_CSS}
|
|
|
275
267
|
.c-draw .dw-t b { font-size:20px; }
|
|
276
268
|
.c-draw .dw-go { margin-left:8px; height:44px; padding:0 16px; border-radius:22px; border:1.5px solid #7a4fc9; background:#fff; color:#7a4fc9; font:inherit; font-size:16px; font-weight:600; }
|
|
277
269
|
#board { flex:1; min-height:0; overflow:auto; padding:calc(env(safe-area-inset-top) + 68px) 16px 24px; display:flex; flex-direction:column; gap:12px; -webkit-overflow-scrolling:touch; scroll-behavior:smooth; }
|
|
278
|
-
.sh .ph { display:inline-block; height:34px; width:auto; max-width:120px; border-radius:6px; object-fit:cover; border:1px solid var(--line); background:#fff; vertical-align:middle; }
|
|
279
270
|
/* 输入条 */
|
|
280
271
|
#bar { padding:8px 16px calc(env(safe-area-inset-bottom) + 12px); }
|
|
281
272
|
#pill { display:flex; align-items:center; gap:12px; height:56px; padding:0 14px; border-radius:28px; background:#fff; border:1px solid var(--line); box-shadow:0 2px 10px #00000010; }
|
|
@@ -385,7 +376,6 @@ __BRAND_CSS__
|
|
|
385
376
|
#lb .nav.l { left:12px; } #lb .nav.r { right:12px; }
|
|
386
377
|
#lb .nav[hidden] { display:none; }
|
|
387
378
|
#lb .n { position:absolute; left:0; right:0; bottom:calc(env(safe-area-inset-bottom) + 18px); text-align:center; color:#fffc; font-size:15px; z-index:2; pointer-events:none; }
|
|
388
|
-
.sh .ph { cursor:zoom-in; }
|
|
389
379
|
/* ---- 按压反馈(全局关了系统的点击高亮,这里统一补):三种手感,按下快(60ms)、松开慢(200ms)。
|
|
390
380
|
圆钮与胶囊:缩到 94% + 暗一档;深色屏上的钮:缩 + 变淡;大块面:缩到 98.5% + 暗一点;行内文字:变淡。
|
|
391
381
|
板书里的卡在滚动容器里,按下延迟 80ms 才显(不然一滑整屏都在闪);卡里的小钮按着时卡自己不动。交互不是皮肤,写在页面里,主题不用跟 ---- */
|
|
@@ -556,7 +546,7 @@ __VOICE_CSS__
|
|
|
556
546
|
body.live #cam, body.live #plus, body.live #lv-mic { display:none !important; }
|
|
557
547
|
/* 按着时按钮自己变蓝(voice-view 的 hold),不再盖一块 #hold 上去 */
|
|
558
548
|
body.live #hold { display:none !important; }
|
|
559
|
-
/*
|
|
549
|
+
/* 按住说话在右边(cotutor.json 的 kid.hand: left,家长端设置页切):白纸挪到右边、板在左边、按钮里头像在右头;回首页、「更多」的位置同右手(回首页浮在板的左上) */
|
|
560
550
|
body.live.hand-left #board { padding-left:calc(env(safe-area-inset-left) + 48px); padding-right:calc(env(safe-area-inset-right) + 400px); }
|
|
561
551
|
body.live.hand-left #stage { left:calc(env(safe-area-inset-left) + 16px); right:calc(env(safe-area-inset-right) + 376px); }
|
|
562
552
|
body.live.hand-left #lv { left:auto; right:0; width:calc(env(safe-area-inset-right) + 360px); padding-top:calc(env(safe-area-inset-top) + 80px); padding-left:24px; padding-right:calc(env(safe-area-inset-right) + 24px); box-shadow:-1px 0 0 #efece4, -6px 0 24px #46371e0d; }
|
|
@@ -827,6 +817,11 @@ ${BOARD_MARK_JS}
|
|
|
827
817
|
h('span', { class: 'lm' }, h('small', { class: 'tg' }, '小课堂'), h('span', { class: 'btx' }, b.label), b.ms ? h('small', { class: 'ln' }, lcLen(b.ms)) : null));
|
|
828
818
|
return h('button', { type: 'button', class: 'bt bt-' + b.kind, 'data-bid': String(b.id), on }, h('span', { class: 'bi', html: ICON[BUTTON_ICON[b.kind]] || ICON.start }), h('span', { class: 'btx' }, b.label));
|
|
829
819
|
};
|
|
820
|
+
// ---- 常驻的三块方砖(预习、检查作业、复习;服务端照老师家里的 activity 文件给,src/ui/kid/home-tiles.ts):新话题下面一行;点了和 activity 按钮一样 ----
|
|
821
|
+
${HOME_TILES_JS}
|
|
822
|
+
const tileButton = (t, x) => h('button', { type: 'button', class: 'bt bt-tile', 'data-tile': x.id, style: '--sc:' + colorOf(t), on: { click: () => pickButton(t, { id: x.id, kind: 'start', label: x.label, resume: x.resume }) } },
|
|
823
|
+
h('span', { class: 'ta', html: tileArt(t.mark, x.id) }), h('span', { class: 'btx' }, x.label));
|
|
824
|
+
const tilesRow = (t, tiles) => (Array.isArray(tiles) && tiles.length ? h('div', { class: 'tls' }, ...tiles.map((x) => tileButton(t, x))) : null);
|
|
830
825
|
/** 挨着的几个小课堂收进一格(平板上两张海报并排,手机上一张一行) */
|
|
831
826
|
const buttonRuns = (t, buttons) => {
|
|
832
827
|
const out = [];
|
|
@@ -939,7 +934,8 @@ ${BOARD_MARK_JS}
|
|
|
939
934
|
const bt = pc ? e.target.closest('.bt') : null;
|
|
940
935
|
if (!bt) return;
|
|
941
936
|
const bid = bt.dataset.bid;
|
|
942
|
-
if (
|
|
937
|
+
if (bt.dataset.tile) pvTell({ act: 'tile', tutor: pc.dataset.tutor, id: bt.dataset.tile });
|
|
938
|
+
else if (bid !== undefined) pvTell({ act: 'button', tutor: pc.dataset.tutor, id: bid === 'new' || bid === 'draft' ? bid : Number(bid) });
|
|
943
939
|
else pvTell({ act: 'extra', tutor: pc.dataset.tutor, kind: bt.classList.contains('bt-x-talk') ? 'talk' : 'dictation' });
|
|
944
940
|
}, true);
|
|
945
941
|
}
|
|
@@ -953,7 +949,8 @@ ${BOARD_MARK_JS}
|
|
|
953
949
|
const { t, card } = list.find((x) => x.t.name === n);
|
|
954
950
|
const buttons = pvButtons(n, Array.isArray(card.props.buttons) ? card.props.buttons : []);
|
|
955
951
|
const owned = (Array.isArray(H.cards) ? H.cards : []).filter((c) => c.kind !== 'tutor' && c.owner === n);
|
|
956
|
-
const
|
|
952
|
+
const tiles = Array.isArray(card.props.tiles) ? card.props.tiles : [];
|
|
953
|
+
const sig = JSON.stringify([t.avatar, t.subject, t.display, buttons, tiles, owned, t.extras, S.talkLast && (S.talkLast.last ? S.talkLast.last.id : 'on'), H.home]);
|
|
957
954
|
let el = K.els.get(n);
|
|
958
955
|
if (!el) {
|
|
959
956
|
const pc = h('div', { class: 'c c-tutor pc', 'data-tutor': n, on: { click: (e) => { if (e.target.closest && e.target.closest('a[href]')) stashFront(pc); if (!pc.classList.contains('front')) pickTutor(n); } } });
|
|
@@ -966,7 +963,8 @@ ${BOARD_MARK_JS}
|
|
|
966
963
|
const others = owned.filter((c) => c.kind !== 'dictation');
|
|
967
964
|
el.replaceChildren(...[
|
|
968
965
|
h('div', { class: 'pc-hd' }, deal ? avatarEl(t, 'draw', 0.1 + slot * 0.12) : avatarEl(t), h('span', { class: 'nm' }, t.display)),
|
|
969
|
-
|
|
966
|
+
// 新话题(第一颗)、方砖一行、家长排的按钮
|
|
967
|
+
h('div', { class: 'kc' }, h('div', { class: 'bts' }, ...((runs) => [runs[0], tilesRow(t, tiles), ...runs.slice(1)])(buttonRuns(t, buttons))), extras.length ? h('div', { class: 'bts extras' }, ...extras) : null),
|
|
970
968
|
others.length ? h('div', { class: 'hcards kcards' }, ...others.map((c, i) => renderCard(c, i, null, false))) : null,
|
|
971
969
|
h('span', { class: 'pc-ix' }, avatarEl(t)),
|
|
972
970
|
h('span', { class: 'veil' }),
|
|
@@ -1190,15 +1188,12 @@ ${BOARD_MARK_JS}
|
|
|
1190
1188
|
const box = (cls, ...kids) => h('div', { class: 'c c-' + cls, 'data-card': idx, 'data-tint': tintFor(c), 'data-look': lookFor(c), on: board ? { click: () => openStage(secIdx, idx) } : {} }, ...kids, lk);
|
|
1191
1189
|
switch (c.kind) {
|
|
1192
1190
|
case 'text': {
|
|
1193
|
-
if (isHeading(c)) return board
|
|
1191
|
+
if (isHeading(c)) return board ? liveHeadEl(p.title || '', secIdx, idx) : h('div', { class: 'heading', 'data-card': idx }, p.title || '');
|
|
1194
1192
|
const emoji = c.look && c.look.emoji ? c.look.emoji + ' ' : '';
|
|
1195
1193
|
const title = p.title ? h('div', { class: 'ct' }, emoji + p.title) : null;
|
|
1196
1194
|
const body = p.text ? h('div', { class: 'cb' }, p.text) : null;
|
|
1197
|
-
// 提问卡:末句那一问(解析器补的)
|
|
1198
|
-
|
|
1199
|
-
const el = box(isAskCard(c) ? 'text c-ask' : 'text', title, body, a ? ansEl(a) : null);
|
|
1200
|
-
if (a) el.dataset.ans = ansKey(a);
|
|
1201
|
-
return el;
|
|
1195
|
+
// 提问卡:末句那一问(解析器补的),和选择题的问题一个字重;孩子的答在左栏,卡上没有答案栏
|
|
1196
|
+
return box(isAskCard(c) ? 'text c-ask' : 'text', title, body);
|
|
1202
1197
|
}
|
|
1203
1198
|
case 'read':
|
|
1204
1199
|
return box('read', ...(p.segments || []).map((seg, k) => h('div', { class: 'rd', on: { click: (e) => { e.stopPropagation(); readSegment(e.currentTarget, c, k); } } }, seg)));
|
|
@@ -1241,10 +1236,10 @@ ${BOARD_MARK_JS}
|
|
|
1241
1236
|
return box('canvas', h('div', { class: 'cp' }, p.prompt || '画一画'), src ? h('div', { class: 'th' + (base ? ' base' : '') }, h('img', { src, alt: '', loading: 'lazy' }), h('span', { class: 'pl' }, n ? '画了 ' + n + ' 笔' : '点开画一画 ✎')) : h('div', { class: 'cb' }, n ? '已经画了 ' + n + ' 笔,点开接着画' : '点开画一画 ✎'));
|
|
1242
1237
|
}
|
|
1243
1238
|
case 'photo': {
|
|
1244
|
-
// 拍给我看(cards/photo/card.md):相机 +
|
|
1239
|
+
// 拍给我看(cards/photo/card.md):相机 + 要拍什么。还没拍:最后一节时下沿「等你拍…」带拍照、相册;拍了:照片放大在卡里,配的话在照片下面(2026-11-04)。点卡不开舞台
|
|
1245
1240
|
const a = board ? ansFor(secIdx, idx) : null;
|
|
1246
|
-
const el = h('div', { class: 'c c-photo', 'data-card': idx, 'data-tint': tintFor(c), 'data-look': lookFor(c) },
|
|
1247
|
-
h('div', { class: 'pc-row' }, h('span', { class: 'pc-ic', html: ICON.camera }), h('div', { class: 'pc-t' }, p.text || '')), a ?
|
|
1241
|
+
const el = h('div', { class: 'c c-photo' + (a && !a.snap ? ' got' : ''), 'data-card': idx, 'data-tint': tintFor(c), 'data-look': lookFor(c) },
|
|
1242
|
+
h('div', { class: 'pc-row' }, h('span', { class: 'pc-ic', html: ICON.camera }), h('div', { class: 'pc-t' }, p.text || '')), a ? (a.snap ? snapEl() : photoAnsEl(a)) : null);
|
|
1248
1243
|
if (a) el.dataset.ans = ansKey(a);
|
|
1249
1244
|
return el;
|
|
1250
1245
|
}
|
|
@@ -1441,9 +1436,6 @@ __WORD_DRAW_JS__
|
|
|
1441
1436
|
const up = (e) => { pts.delete(e.pointerId); start = pts.size ? { scale, tx, ty, d: pts.size === 2 ? dist() : 0, x: [...pts.values()][0].x, y: [...pts.values()][0].y } : null; };
|
|
1442
1437
|
el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up);
|
|
1443
1438
|
};
|
|
1444
|
-
// 节头:时间 · 节名;孩子问这节时拍的照片(R5)缩略图跟在后面,点小图看大图(不是再听)
|
|
1445
|
-
const headPhotos = (s) => (s.photos || []).map((p, k) => h('img', { class: 'ph', src: '/api/kid/image?p=' + encodeURIComponent(p), alt: '', loading: 'lazy', on: { click: (e) => { e.stopPropagation(); openLb(s.photos, k); } } }));
|
|
1446
|
-
const sectionHead = (s) => h('div', { class: 'sh' }, (s.at ? clock(s.at) + ' · ' : '') + sectionTitle(s), ...headPhotos(s));
|
|
1447
1439
|
const rowEl = (n, ...kids) => h('div', { class: 'row', style: 'grid-template-columns:repeat(' + n + ',minmax(0,1fr))' }, ...kids);
|
|
1448
1440
|
// ---- 排版:每张卡放进量具量一次,半宽放得下(fitsHalf)就半宽,相邻两张半宽的并一行(rowsFor) ----
|
|
1449
1441
|
// 量具在 body 下、屏幕外:板书藏着(首页开着)时也量得出;按 端 + 半宽像素 + 卡的内容缓存,看录像每次重画不再量
|
|
@@ -1594,62 +1586,54 @@ __WORD_DRAW_JS__
|
|
|
1594
1586
|
hideLecture(); renderLecturePending(); renderBar(); renderSubtitle();
|
|
1595
1587
|
} else if (m.type === 'error') { const first = S.lecture && !S.lecture.watch; hideLecture(); if (first) closeTutor(); }
|
|
1596
1588
|
});
|
|
1597
|
-
// ----
|
|
1598
|
-
|
|
1599
|
-
const saidBody = (sd, on = {}) => [
|
|
1600
|
-
// 答拍给我看卡拍来的照片(kidSaid 的 photos):缩在答案栏里,点开看大图;只发了照片没说话就没有字
|
|
1601
|
-
...(sd.photos || []).map((p, k) => h('img', { class: 'kp', src: '/api/kid/image?p=' + encodeURIComponent(p), alt: '', loading: 'lazy', on: { click: (e) => { e.stopPropagation(); openLb(sd.photos, k); } } })),
|
|
1602
|
-
sd.text ? h('span', { class: 'ka-t', on }, sd.text) : null];
|
|
1603
|
-
/** 点开看原话(语音的显示是去了口头禅的),再点收起 */
|
|
1604
|
-
const toggleSaid = (sd) => (e) => { e.stopPropagation(); const el = e.currentTarget.closest('.kid-ans, .sh'); const t = el.querySelector('.ka-t'); if (!t) return; const open = el.classList.toggle('open'); t.textContent = open ? sd.full : sd.text; };
|
|
1605
|
-
/** 孩子单独说的那句当节头:话 —— 线 —— [时间] [照片] 再听;meta = false 只要话和线(刚发出去挂在板尾的、小课堂卡下面的) */
|
|
1606
|
-
const saidHead = (s, sd, meta, first) => h('div', { class: 'sh said' }, ...saidBody(sd, { click: toggleSaid(sd) }), h('span', { class: 'ln' }), ...(meta ? [first && s.at ? clock(s.at) : null, ...headPhotos(s)] : []));
|
|
1607
|
-
/** 一节开头:孩子没单独说话 → 原来的节头(只留第一节与带照片的);说了 → 他的话就是节头;看完小课堂后的第一问写在小课堂卡下面 */
|
|
1589
|
+
// ---- 一个话题一节(板上只有实时一种画法,2026-11-04;以前的话题也这样画):节头是脑写的那张「# 标题」卡,孩子的话不当节头(在左栏 #lv-kid);
|
|
1590
|
+
// 答拍给我看卡的照片与话放进那张卡(photoAnsEl),别的卡没有答案栏 ----
|
|
1608
1591
|
/** 孩子从纸上交的画:节前一张——图(点开看大图)、几笔、「接着画」打开这个话题的纸(只读、家长端、录像里不出) */
|
|
1609
1592
|
const drawHead = (s) => !s.drawing ? null : h('div', { class: 'c c-draw' },
|
|
1610
1593
|
h('img', { class: 'dw-th', src: '/api/kid/image?p=' + encodeURIComponent(s.drawing.image), alt: '', loading: 'lazy', on: { click: (e) => { e.stopPropagation(); openLb([s.drawing.image], 0); } } }),
|
|
1611
1594
|
h('div', { class: 'dw-t' }, h('small', {}, '我画的'), h('b', {}, '画了 ' + s.drawing.strokes + ' 笔')),
|
|
1612
1595
|
!S.readonly && !REEL ? h('button', { class: 'dw-go', type: 'button', on: { click: (e) => { e.stopPropagation(); openPaper(); } } }, '接着画') : null);
|
|
1613
|
-
|
|
1614
|
-
const
|
|
1615
|
-
// 实时:节头是脑写的那张卡(liveHeadEl,随卡画);板上第一节没写节头,只挂时间。孩子的话不当节头(在左栏 #lv-kid)
|
|
1616
|
-
if (s.live) return [i === 0 && !s.head && s.at ? h('div', { class: 'sh lh' }, h('span', { class: 'ln' }), h('span', { class: 'tm' }, clock(s.at))) : null, ...lectureHead(s)];
|
|
1617
|
-
const sd = s.said && !s.saidOn ? s.said : null;
|
|
1618
|
-
const lec = lectureHead(s);
|
|
1619
|
-
if (!sd) return [headOf(s, i), ...lec];
|
|
1620
|
-
if (!lec.some(Boolean)) return [saidHead(s, sd, true, i === 0)];
|
|
1621
|
-
return [headOf(s, i), ...lec, saidHead(s, sd, false)];
|
|
1622
|
-
};
|
|
1623
|
-
/** 实时的一节:节头脑写、孩子的话不进答案栏、卡和讲稿不同步 */
|
|
1624
|
-
const liveSec = (i) => Boolean(i !== null && S.sections[i] && S.sections[i].live);
|
|
1596
|
+
/** 节头是脑写的那张卡(liveHeadEl,随卡画);板上第一节没写节头,只挂时间 */
|
|
1597
|
+
const headEls = (s, i) => [i === 0 && !s.head && s.at ? h('div', { class: 'sh lh' }, h('span', { class: 'ln' }), h('span', { class: 'tm' }, clock(s.at))) : null, ...lectureHead(s), drawHead(s)];
|
|
1625
1598
|
/** 脑写的节头(只有一行「# 标题」的文字卡):一行摘要 —— 线 —— 板上第一节带时间;一轮中间的也照这样断成新一节 */
|
|
1626
1599
|
const liveHeadEl = (title, i, k) => { const s = S.sections[i]; return h('div', { class: 'sh lh', 'data-card': k }, h('span', { class: 't' }, title), h('span', { class: 'ln' }), i === 0 && k === 0 && s && s.at ? h('span', { class: 'tm' }, clock(s.at)) : null); };
|
|
1627
|
-
/**
|
|
1600
|
+
/** 卡落下:讲稿里标这张卡的 [词] 都画上(不等念到、卡和讲稿不同步),田字格、单词卡写一遍(这一页只写一次);animate = false 是重画、回来看,只画不写 */
|
|
1628
1601
|
const liveLand = (i, k, animate) => {
|
|
1629
|
-
const s = S.sections[i]; if (!s
|
|
1602
|
+
const s = S.sections[i]; if (!s) return;
|
|
1630
1603
|
for (const l of s.lines) for (const m of l.marks) if (m.card === k) applyMark(i, m, animate);
|
|
1631
1604
|
const el = animate ? $('#board').querySelector('[data-sec="' + i + '"] [data-card="' + k + '"]') : null;
|
|
1632
1605
|
if (el && (el.classList.contains('c-tianzige') || el.classList.contains('c-word')) && !el.dataset.wrote) { el.dataset.wrote = '1'; if (el.classList.contains('c-word')) wordWrite(el.querySelector('.wg'), false, null); else tianzigePlay(el); }
|
|
1633
1606
|
};
|
|
1634
|
-
/**
|
|
1607
|
+
/**
|
|
1608
|
+
* 拍给我看卡上孩子答的(只有它有):答了 = 他拍来的照片与配的话(没拍、直接说的那句也算);刚发出去、老师还没写出下一节的也先放上;
|
|
1609
|
+
* 最后一节的这张还没人答 = 等你拍(拍照、相册;拍照键只在这里,没有它这张卡点不动)。别的卡没有答案栏,孩子的话在左栏
|
|
1610
|
+
*/
|
|
1635
1611
|
const ansFor = (j, k) => {
|
|
1636
1612
|
const sec = S.sections[j];
|
|
1637
|
-
if (!sec || sec.
|
|
1613
|
+
if (!sec || !sec.cards[k] || sec.cards[k].kind !== 'photo') return null;
|
|
1638
1614
|
for (const s of S.sections) if (s.said && s.saidOn && s.saidOn.job === sec.job && s.saidOn.card === k) return s.said;
|
|
1639
1615
|
if (S.pendSaid && S.pendSaid.on && S.pendSaid.on.job === sec.job && S.pendSaid.on.card === k) return S.pendSaid.said;
|
|
1640
|
-
if ((!S.readonly || REEL) && !S.pendSaid && j === S.sections.length - 1 && askCard(sec) === k) return
|
|
1616
|
+
if ((!S.readonly || REEL) && !S.pendSaid && j === S.sections.length - 1 && askCard(sec) === k) return { snap: true };
|
|
1641
1617
|
return null;
|
|
1642
1618
|
};
|
|
1643
|
-
/**
|
|
1644
|
-
const ansKey = (a) => (a.snap ? 'snap' :
|
|
1619
|
+
/** 卡上画的是什么(data-ans):变了才重画那张卡 */
|
|
1620
|
+
const ansKey = (a) => (a.snap ? 'snap' : (a.text || '') + '|' + (a.photos || []).join(','));
|
|
1645
1621
|
/** 拍给我看卡上的拍照 / 相册:一个藏起来的 file input,和输入条的相机同一套(wireFile) */
|
|
1646
1622
|
const snapBtn = (camera) => {
|
|
1647
1623
|
const f = h('input', camera ? { type: 'file', accept: 'image/*', capture: 'environment' } : { type: 'file', accept: 'image/*', multiple: '' });
|
|
1648
1624
|
wireFile(f);
|
|
1649
1625
|
return h('label', { class: 'pc-go' + (camera ? '' : ' alt'), on: { click: (e) => e.stopPropagation() } }, h('span', { html: camera ? ICON.camera : ICON.album }), camera ? '拍照' : '相册', f);
|
|
1650
1626
|
};
|
|
1651
|
-
|
|
1652
|
-
|
|
1627
|
+
/** 等你拍:卡下沿一条,拍照、相册 */
|
|
1628
|
+
const snapEl = () => h('div', { class: 'kid-ans snap' }, h('span', { class: 'ka-w' }, '等你拍…'), snapBtn(true), snapBtn(false));
|
|
1629
|
+
/** 拍来的:照片放大在卡里(一张整宽、整张看得见;几张两列),点开看大图;配的话在照片下面,像图注;点话看原话(语音的显示去了口头禅) */
|
|
1630
|
+
const photoAnsEl = (a) => {
|
|
1631
|
+
const ps = a.photos || [];
|
|
1632
|
+
const words = a.text ? h('div', { class: 'pc-said', on: { click: (e) => { e.stopPropagation(); const el = e.currentTarget; const open = el.classList.toggle('open'); el.textContent = open ? a.full : a.text; } } }, a.text) : null;
|
|
1633
|
+
return h('div', { class: 'pc-ans' },
|
|
1634
|
+
ps.length ? h('div', { class: 'pc-shots' + (ps.length === 1 ? ' one' : '') }, ...ps.map((p, k) => h('img', { src: '/api/kid/image?p=' + encodeURIComponent(p), alt: '', loading: 'lazy', on: { click: (e) => { e.stopPropagation(); openLb(ps, k); } } }))) : null,
|
|
1635
|
+
words);
|
|
1636
|
+
};
|
|
1653
1637
|
/** 拿起相机老师就停(同点卡开舞台);拍完 / 选完进发照片屏。相册最多 4 张,多的只取前 4 张。输入条、加号面板、拍给我看卡都走这里 */
|
|
1654
1638
|
const wireFile = (f) => {
|
|
1655
1639
|
f.addEventListener('change', () => { $('#sheet').classList.remove('on'); const files = Array.from(f.files || []).slice(0, f.multiple ? PHOTO_ALBUM_MAX : 1); f.value = ''; psOpen(files); });
|
|
@@ -1671,22 +1655,11 @@ __WORD_DRAW_JS__
|
|
|
1671
1655
|
if ((el.dataset.ans || '') !== (a ? ansKey(a) : '')) repaintCard(j, k);
|
|
1672
1656
|
}
|
|
1673
1657
|
};
|
|
1674
|
-
|
|
1675
|
-
const syncPendSay = () => {
|
|
1676
|
-
const old = $('#board > .sh.said.pend');
|
|
1677
|
-
const p = S.pendSaid && !S.pendSaid.on && !S.pendSaid.live && !LV.on ? S.pendSaid : null;
|
|
1678
|
-
if (old && (!p || old.dataset.said !== p.job)) old.remove();
|
|
1679
|
-
if (!p || (old && old.dataset.said === p.job)) return;
|
|
1680
|
-
const el = saidHead(null, p.said, false); el.classList.add('pend'); el.dataset.said = p.job;
|
|
1681
|
-
$('#board').append(el);
|
|
1682
|
-
};
|
|
1683
|
-
const renderSection = (s, i) => h('div', { class: 'sec' + (s.live && s.cont ? ' cont' : ''), 'data-sec': i, 'data-job': s.job }, ...headEls(s, i), ...rowEls(s, s.cards.map((c, k) => renderCard(c, k, i, false))).map((r) => r.el));
|
|
1658
|
+
const renderSection = (s, i) => h('div', { class: 'sec' + (s.cont ? ' cont' : ''), 'data-sec': i, 'data-job': s.job }, ...headEls(s, i), ...rowEls(s, s.cards.map((c, k) => renderCard(c, k, i, false))).map((r) => r.el));
|
|
1684
1659
|
/**
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
* 铺卡在拍就绪时而不是围栏闭合时:铺的时候声音已齐,不会先素后彩地闪
|
|
1660
|
+
* 老师还在写:卡闭合了就铺、有句就出节(讲稿嘴在念,不等拍),转成 S.sections 里的一节(partial);之后每次 poll 只铺新闭合的卡、把 lines 换成最新的;
|
|
1661
|
+
* 写完了 finalizeLive 换成正式的一节
|
|
1688
1662
|
*/
|
|
1689
|
-
const liveCardsOf = (e) => { if (e.live) return e.cards.length; const bs = beatsOf(e).slice(0, e.ready || 0); let n = 0; for (const b of bs) if (b.card !== null) n = Math.max(n, b.card + 1); return n; };
|
|
1690
1663
|
/** 一张卡落到行里:和前面的卡并一行(半宽 + 半宽,或明写的并排)就进那一行,否则新起一行;只看这张和前面的,后面的卡不影响前面的行 */
|
|
1691
1664
|
// ---- 跟到底:新露出来的卡在板的最后 → 板滚到底(连底下留的空一起露);之后这一节再长高(问题卡的「等你说…」、图片载完、排版量完),
|
|
1692
1665
|
// 只要孩子没自己往上翻,就再滚到底。原来是 scrollIntoView nearest:只把卡的下沿贴到可视区下沿,之后长高的那截就在屏幕外
|
|
@@ -1735,14 +1708,14 @@ __WORD_DRAW_JS__
|
|
|
1735
1708
|
const renderLive = (e) => {
|
|
1736
1709
|
if (!S.partial || S.partial.job !== e.job) { if (S.partial) S.partial.el.remove(); const idx = S.sections.length; S.partial = { job: e.job, idx, live: false, shown: 0, el: h('div', { class: 'sec', 'data-sec': idx, 'data-job': e.job }, ...headEls(e, idx)) }; $('#board').append(S.partial.el); }
|
|
1737
1710
|
const P = S.partial, idx = P.idx;
|
|
1738
|
-
P.el.classList.toggle('cont', Boolean(e.
|
|
1739
|
-
//
|
|
1740
|
-
if (!(e.
|
|
1711
|
+
P.el.classList.toggle('cont', Boolean(e.cont));
|
|
1712
|
+
// 卡闭合了就铺、有句就出节(讲稿嘴在念,不等拍)
|
|
1713
|
+
if (!(e.cards.length || e.lines.length)) return;
|
|
1741
1714
|
const sec = { ...e, partial: true };
|
|
1742
1715
|
const wasLive = P.live;
|
|
1743
1716
|
if (!wasLive) { P.live = true; S.played.add(e.job); S.sections.push(sec); }
|
|
1744
1717
|
else S.sections[idx] = sec;
|
|
1745
|
-
const n =
|
|
1718
|
+
const n = sec.cards.length;
|
|
1746
1719
|
const placed = n > P.shown;
|
|
1747
1720
|
for (; P.shown < n; P.shown++) { follow(placeCard(P, sec, P.shown, idx)); liveLand(idx, P.shown, true); }
|
|
1748
1721
|
dispatch({ type: wasLive ? 'liveBeat' : 'liveStart', section: idx });
|
|
@@ -1755,12 +1728,12 @@ __WORD_DRAW_JS__
|
|
|
1755
1728
|
const sec = { ...e };
|
|
1756
1729
|
S.sections[idx] = sec;
|
|
1757
1730
|
const el = P.el;
|
|
1758
|
-
el.classList.toggle('cont', Boolean(sec.
|
|
1731
|
+
el.classList.toggle('cont', Boolean(sec.cont));
|
|
1759
1732
|
for (let k = 0; k < sec.cards.length; k++) {
|
|
1760
1733
|
const old = k < P.shown ? el.querySelector('[data-card="' + k + '"]') : null;
|
|
1761
1734
|
if (old) swapCard(old, renderCard(sec.cards[k], k, idx, false));
|
|
1762
1735
|
else placeCard(P, sec, k, idx);
|
|
1763
|
-
//
|
|
1736
|
+
// 换了样子的卡标注补画,新铺的照落下画、写
|
|
1764
1737
|
liveLand(idx, k, !old);
|
|
1765
1738
|
}
|
|
1766
1739
|
dispatch({ type: 'liveFinal', section: idx });
|
|
@@ -1795,7 +1768,6 @@ __WORD_DRAW_JS__
|
|
|
1795
1768
|
if (!el) return;
|
|
1796
1769
|
el.classList.add('now');
|
|
1797
1770
|
};
|
|
1798
|
-
const showNow = () => setNow(S.state.section, nowCard(S.sections, S.state));
|
|
1799
1771
|
/** 换端(转屏)或窗口变了:端或半宽变了就整节重排(卡重新量),标注画齐,选中态照旧 */
|
|
1800
1772
|
const relayout = () => {
|
|
1801
1773
|
const d = debug.get('device') || deviceFor(innerWidth, innerHeight);
|
|
@@ -1805,7 +1777,7 @@ __WORD_DRAW_JS__
|
|
|
1805
1777
|
if (d !== S.device || hw !== S.layHalf) {
|
|
1806
1778
|
S.device = d; S.layHalf = hw;
|
|
1807
1779
|
S.sections.forEach((s, i) => { const old = $('#board').querySelector('[data-sec="' + i + '"]'); if (old && !(S.partial && S.partial.el === old)) { old.replaceWith(renderSection(s, i)); paintAll(i); } });
|
|
1808
|
-
if (S.stage) setNow(S.stage.section, S.stage.card);
|
|
1780
|
+
if (S.stage) setNow(S.stage.section, S.stage.card);
|
|
1809
1781
|
} else for (const span of document.querySelectorAll('#board .mk[data-pen]')) { const card = span.closest('.c'); if (card && LINE_PENS.includes(span.dataset.pen)) drawPen(span, card, span.dataset.pen, false); }
|
|
1810
1782
|
};
|
|
1811
1783
|
let relayoutTimer = null;
|
|
@@ -2037,7 +2009,7 @@ __WORD_DRAW_JS__
|
|
|
2037
2009
|
const closeStage = () => { if (S.stage && S.sections[S.stage.section]) playRec({ k: 'stage', job: S.sections[S.stage.section].job, card: S.stage.card, open: false });
|
|
2038
2010
|
// 单词卡舞台:自动那一遍停下,慢念也停
|
|
2039
2011
|
for (const v of document.querySelectorAll('#st-body video')) { try { v.pause(); } catch {} }
|
|
2040
|
-
wordRun++; if (S.stage && !S.reel && S.sections[S.stage.section] && (S.sections[S.stage.section].cards[S.stage.card] || {}).kind === 'word') silence(); S.stage = null; frame.src = 'about:blank'; canvasClosed(); $('#stage').classList.remove('on'); renderSubtitle();
|
|
2012
|
+
wordRun++; if (S.stage && !S.reel && S.sections[S.stage.section] && (S.sections[S.stage.section].cards[S.stage.card] || {}).kind === 'word') silence(); S.stage = null; frame.src = 'about:blank'; canvasClosed(); $('#stage').classList.remove('on'); renderSubtitle(); setNow(null, null); };
|
|
2041
2013
|
$('#st-x').innerHTML = ICON.close;
|
|
2042
2014
|
// 孩子关:讲稿交给小课堂卡、还没放完的,关了接着念
|
|
2043
2015
|
const closeByKid = () => { const d = S.stage; const card = d && S.sections[d.section] ? S.sections[d.section].cards[d.card] : null; closeStage(); if (d && d.delegate && card && (!isHeavy(card) || card.kind === 'lecture')) resumeAfter(); };
|
|
@@ -2127,7 +2099,8 @@ __WORD_DRAW_JS__
|
|
|
2127
2099
|
document.body.classList.toggle('speaking', status === 'mouth');
|
|
2128
2100
|
if (LV.was === 'mouth' && (LV.on ? status !== 'mouth' : status === 'waiting' || status === 'done') && !S.pending) liveNod();
|
|
2129
2101
|
LV.was = status;
|
|
2130
|
-
|
|
2102
|
+
// 录像里孩子的屏幕:老师的字幕由 reelLiveCap 照那一刻画,孩子刚说的那句照孩子端一样写在左栏(板上不挂孩子的话)
|
|
2103
|
+
if (!REEL) liveCaption(); else lvKid();
|
|
2131
2104
|
voiceSync();
|
|
2132
2105
|
};
|
|
2133
2106
|
|
|
@@ -2140,7 +2113,6 @@ __WORD_DRAW_JS__
|
|
|
2140
2113
|
switch (f.kind) {
|
|
2141
2114
|
case 'stop': silence(); break;
|
|
2142
2115
|
case 'render': renderSubtitle(); break;
|
|
2143
|
-
case 'showNow': if (!LV.on) showNow(); break;
|
|
2144
2116
|
case 'paint': paintAll(f.section); break;
|
|
2145
2117
|
case 'scrollLast': { pin.on = false; const last = $('#board').querySelector('[data-sec="' + (S.sections.length - 1) + '"] .c'); if (last) last.scrollIntoView({ block: 'start', behavior: 'instant' }); break; }
|
|
2146
2118
|
}
|
|
@@ -2230,9 +2202,7 @@ __WORD_DRAW_JS__
|
|
|
2230
2202
|
const loadDay = async (silent) => {
|
|
2231
2203
|
if (!S.tutor || S.reel) return;
|
|
2232
2204
|
try {
|
|
2233
|
-
|
|
2234
|
-
const warmHint = !S.hist && S.thread ? '?thread=' + encodeURIComponent(S.thread) : '';
|
|
2235
|
-
const d = await api('GET', CONV + S.tutor.name + '/' + (S.hist || 'today') + warmHint);
|
|
2205
|
+
const d = await api('GET', CONV + S.tutor.name + '/' + (S.hist || 'today'));
|
|
2236
2206
|
setOffline(false);
|
|
2237
2207
|
S.day = d;
|
|
2238
2208
|
// 话题:没选过就是当前(末条所在)的;板书只铺这个话题的节。新话题空白态什么都不铺,发出第一句才有话题
|
|
@@ -2255,21 +2225,21 @@ __WORD_DRAW_JS__
|
|
|
2255
2225
|
const el = S.partial.el; S.partial = null;
|
|
2256
2226
|
el.replaceWith(renderSection(e, idx));
|
|
2257
2227
|
} else $('#board').append(renderSection(e, idx));
|
|
2258
|
-
//
|
|
2259
|
-
|
|
2228
|
+
// 一节整节到了(没流过):标注都画上;不是回来看(silent)就当刚落下,田字格写一遍
|
|
2229
|
+
e.cards.forEach((_, k) => liveLand(idx, k, !silent));
|
|
2260
2230
|
}
|
|
2261
2231
|
// 这条既不 pending 也没定稿(运行出错了):撤掉;live 的那节也撤(孩子端出错的运行不出现)
|
|
2262
2232
|
if (S.partial && !entries.some((e) => e.job === S.partial.job)) { const P = S.partial; S.partial = null; P.el.remove(); if (P.live) { S.sections.splice(P.idx, 1); dispatch({ type: 'liveDropped' }); } }
|
|
2263
2233
|
// 服务端的状态是真相(别的设备上选的、重开页面):没在舞台里改着的卡照它画;props 也跟(小课堂卡的 ready、起止是服务端下发时现读的)
|
|
2264
2234
|
entries.forEach((e) => { const i = S.sections.findIndex((x) => x.job === e.job); if (i < 0 || fresh.includes(i)) return; e.cards.forEach((c, idx) => { const mine = S.sections[i].cards[idx]; if (!mine || (S.stage && S.stage.section === i && S.stage.card === idx)) return; const ds = JSON.stringify(mine.state) !== JSON.stringify(c.state), dp = JSON.stringify(mine.props) !== JSON.stringify(c.props); if (ds || dp) { mine.state = c.state; mine.props = c.props; repaintCard(i, idx); } }); });
|
|
2265
2235
|
if (fresh.length && S.stage && !S.stage.paper) closeStage();
|
|
2266
|
-
//
|
|
2236
|
+
// 孩子的话:刚发出去、这一节还没出来的记着(左栏照它写;答拍给我看卡的先放进那张卡)
|
|
2267
2237
|
{
|
|
2268
2238
|
const lastM = mine[mine.length - 1];
|
|
2269
2239
|
const sd = lastM && lastM.pending ? kidSaid(mine).get(lastM.job) : null;
|
|
2270
2240
|
const shown = lastM && (S.sections.some((s) => s.job === lastM.job) || (S.partial && S.partial.job === lastM.job));
|
|
2271
|
-
S.pendSaid = sd && !shown ? { job: lastM.job, said: sd.said, on: sd.on
|
|
2272
|
-
|
|
2241
|
+
S.pendSaid = sd && !shown ? { job: lastM.job, said: sd.said, on: sd.on } : null;
|
|
2242
|
+
syncAnswers();
|
|
2273
2243
|
}
|
|
2274
2244
|
const stillPending = Boolean(d.pending) || d.messages.some((m) => m.pending);
|
|
2275
2245
|
S.pending = stillPending;
|
|
@@ -2452,7 +2422,7 @@ __WORD_DRAW_JS__
|
|
|
2452
2422
|
const lastN = f.notes[f.notes.length - 1];
|
|
2453
2423
|
const sd = lastN && !shown.some((fs) => fs.job === lastN.job) ? R.said.get(lastN.job) : null;
|
|
2454
2424
|
const pk = sd ? lastN.job : '';
|
|
2455
|
-
if (pk !== R.pend || R.rebuilt) { R.pend = pk; S.pendSaid = sd ? { job: lastN.job, said: sd.said, on: sd.on } : null;
|
|
2425
|
+
if (pk !== R.pend || R.rebuilt) { R.pend = pk; S.pendSaid = sd ? { job: lastN.job, said: sd.said, on: sd.on } : null; syncAnswers(); }
|
|
2456
2426
|
let fresh = null;
|
|
2457
2427
|
shown.forEach((fs, i) => {
|
|
2458
2428
|
const el = $('#board').querySelector(':scope > .sec[data-sec="' + i + '"]'); if (!el) return;
|
|
@@ -2470,16 +2440,11 @@ __WORD_DRAW_JS__
|
|
|
2470
2440
|
for (const l of sec.lines) for (const m of l.marks) if (m.card === k) applyMark(i, m, false);
|
|
2471
2441
|
}
|
|
2472
2442
|
});
|
|
2473
|
-
// 念到哪张亮哪张
|
|
2474
|
-
const at = f.saying || f.last;
|
|
2475
|
-
const i = at ? S.sections.findIndex((x) => x.job === at.job) : -1;
|
|
2476
|
-
const nk = i >= 0 ? i + '/' + at.line : '';
|
|
2477
|
-
if (nk !== R.now) { R.now = nk; if (i >= 0 && !S.sections[i].live) { setNow(i, nowCard(S.sections, { section: i, line: at.line, status: 'playing' })); const el = $('#board .c.now'); if (el) fresh = el; } }
|
|
2478
2443
|
// 跟着最新的走:板上看得见的多了(新的一节、刚发出去的话、露出来的卡)或刚拖过进度条,就滚到最底下那个;只是念到了上面另一张卡,滚到那张
|
|
2479
2444
|
const board = $('#board');
|
|
2480
|
-
const seen = board.querySelectorAll(':scope > .sec, :scope > .
|
|
2445
|
+
const seen = board.querySelectorAll(':scope > .sec, :scope > .sec .c:not(.pend)').length;
|
|
2481
2446
|
if (seen > R.seen || R.follow) {
|
|
2482
|
-
const tail = [...board.querySelectorAll(':scope > .sec
|
|
2447
|
+
const tail = [...board.querySelectorAll(':scope > .sec')].reverse().find((el) => el.getClientRects().length);
|
|
2483
2448
|
if (tail) tail.scrollIntoView({ block: 'end', behavior: R.follow ? 'instant' : 'smooth' });
|
|
2484
2449
|
} else if (fresh) fresh.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
2485
2450
|
R.seen = seen; R.follow = false;
|
|
@@ -3045,7 +3010,7 @@ __REEL_DOCK_JS__
|
|
|
3045
3010
|
const lvKid = () => {
|
|
3046
3011
|
const el = $('#lv-kid');
|
|
3047
3012
|
const last = S.sections[S.sections.length - 1];
|
|
3048
|
-
const p = S.pendSaid ? { job: S.pendSaid.job, sd: S.pendSaid.said } : last && last.
|
|
3013
|
+
const p = S.pendSaid ? { job: S.pendSaid.job, sd: S.pendSaid.said } : last && last.said ? { job: last.job, sd: last.said } : null;
|
|
3049
3014
|
if (!p || !p.sd || !p.sd.text) { if (el.firstChild) el.replaceChildren(); delete el.dataset.k; return; }
|
|
3050
3015
|
const old = LV.heard.has(p.job) || !S.pending;
|
|
3051
3016
|
const key = p.job + '|' + p.sd.text;
|
|
@@ -3056,7 +3021,7 @@ __REEL_DOCK_JS__
|
|
|
3056
3021
|
// (孩子按下时清掉、没放出来的也在),没有(以前的轮、mock 以外没念过)就不露。谁让谁在 liveStep;这里只取、放、让字幕跟着走 ----
|
|
3057
3022
|
const RP = { job: null, data: null, miss: false, loading: false, el: null, timer: 0, line: -1 };
|
|
3058
3023
|
/** 这一轮:板上最后一节实时的、有讲稿的 */
|
|
3059
|
-
const replaySec = () => { for (let i = S.sections.length - 1; i >= 0; i--) { const s = S.sections[i]; if (s.
|
|
3024
|
+
const replaySec = () => { for (let i = S.sections.length - 1; i >= 0; i--) { const s = S.sections[i]; if (s.lines.length) return s; } return null; };
|
|
3060
3025
|
const replayUi = () => {
|
|
3061
3026
|
const b = $('#lv-replay');
|
|
3062
3027
|
const s = LV.on ? replaySec() : null;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 老师的:头像旁紧挨着三道小弧「)))」,老师的颜色,不走、不放大,只有深浅跟老师的音量(平滑、三道依次错开一点);没声音淡到最低。
|
|
8
8
|
* 10-21 Jack:有字幕在变、有声音,老师这边动画太强会把孩子的注意力从内容上拉走——所以只留这么一点,头像也不再一圈圈放大。
|
|
9
9
|
* - 孩子的:竖条从按钮另一头往头像里走,高低跟麦克风音量,走到头像收进去、头像外圈亮一下;按着没声音是小点慢慢走。
|
|
10
|
-
* 插话(一直听时孩子开口而老师还在说)两样都在,老师的淡到三成。镜像(
|
|
10
|
+
* 插话(一直听时孩子开口而老师还在说)两样都在,老师的淡到三成。镜像(按住说话在右边,kid.hand: left)时方向反过来。系统「减弱动态效果」时竖条不走,只按音量变高低。
|
|
11
11
|
*
|
|
12
12
|
* 要改动画:先改下面的 VOICE_TUNE(快慢、深浅、大小、颜色都在这里);样子的种类在 voice-view.ts;打开 /dev/voice 一页看全部。
|
|
13
13
|
* VOICE_JS 插进页面的模板字符串:不写反斜杠、反引号、美元加花括号(AGENTS.md「坑」)。
|
package/dist/ui/kid/voice-ui.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 老师的:头像旁紧挨着三道小弧「)))」,老师的颜色,不走、不放大,只有深浅跟老师的音量(平滑、三道依次错开一点);没声音淡到最低。
|
|
8
8
|
* 10-21 Jack:有字幕在变、有声音,老师这边动画太强会把孩子的注意力从内容上拉走——所以只留这么一点,头像也不再一圈圈放大。
|
|
9
9
|
* - 孩子的:竖条从按钮另一头往头像里走,高低跟麦克风音量,走到头像收进去、头像外圈亮一下;按着没声音是小点慢慢走。
|
|
10
|
-
* 插话(一直听时孩子开口而老师还在说)两样都在,老师的淡到三成。镜像(
|
|
10
|
+
* 插话(一直听时孩子开口而老师还在说)两样都在,老师的淡到三成。镜像(按住说话在右边,kid.hand: left)时方向反过来。系统「减弱动态效果」时竖条不走,只按音量变高低。
|
|
11
11
|
*
|
|
12
12
|
* 要改动画:先改下面的 VOICE_TUNE(快慢、深浅、大小、颜色都在这里);样子的种类在 voice-view.ts;打开 /dev/voice 一页看全部。
|
|
13
13
|
* VOICE_JS 插进页面的模板字符串:不写反斜杠、反引号、美元加花括号(AGENTS.md「坑」)。
|
|
@@ -45,12 +45,14 @@ export async function wordEmoji(words, opts = {}) {
|
|
|
45
45
|
return {};
|
|
46
46
|
const model = opts.model ?? EMOJI_MODEL;
|
|
47
47
|
const line = qianwenLine(model, qianwenKeys(opts.env ?? process.env));
|
|
48
|
-
|
|
48
|
+
// 指了端点的是测试与探针的假服务,不看 key
|
|
49
|
+
const url = opts.endpoint ?? (line && chatUrl(line.host));
|
|
50
|
+
if (!url)
|
|
49
51
|
return {};
|
|
50
52
|
try {
|
|
51
|
-
const res = await fetch(
|
|
53
|
+
const res = await fetch(url, {
|
|
52
54
|
method: 'POST',
|
|
53
|
-
headers: { authorization: `Bearer ${line
|
|
55
|
+
headers: { authorization: `Bearer ${line?.key ?? 'sk-test'}`, 'content-type': 'application/json' },
|
|
54
56
|
body: JSON.stringify({ model, reasoning_effort: 'none', messages: [{ role: 'user', content: emojiPrompt(words) }] }),
|
|
55
57
|
signal: AbortSignal.timeout(opts.timeoutMs ?? EMOJI_TIMEOUT_MS),
|
|
56
58
|
});
|