From 9b28b2ff7bf87d7025bd83dbea9f02e35e2af910 Mon Sep 17 00:00:00 2001 From: hash-panda <110599333+hash-panda@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:56:09 +0800 Subject: [PATCH] Add reviewed Embedding lesson with exact lookup, vector comparison and learner checks --- apps/course/catalog.json | 6 +-- apps/site/learn/embedding/LESSON.md | 43 +++++++++++++++++++ apps/site/learn/embedding/SCENE_SPEC.json | 17 ++++++++ apps/site/learn/embedding/app.mjs | 25 +++++++++++ apps/site/learn/embedding/index.html | 3 ++ apps/site/learn/embedding/model.mjs | 15 +++++++ apps/site/learn/embedding/model.test.mjs | 15 +++++++ apps/site/learn/embedding/stage.mjs | 43 +++++++++++++++++++ apps/site/learn/embedding/style.css | 20 +++++++++ docs/ai-learning/STATUS.md | 7 ++- .../releases/2026-10-27-embedding.md | 7 +++ docs/ai-learning/roadmap.json | 6 +-- evals/browser/embedding-lifecycle.html | 5 +++ package.json | 2 +- scripts/build.mjs | 2 +- 15 files changed, 207 insertions(+), 9 deletions(-) create mode 100644 apps/site/learn/embedding/LESSON.md create mode 100644 apps/site/learn/embedding/SCENE_SPEC.json create mode 100644 apps/site/learn/embedding/app.mjs create mode 100644 apps/site/learn/embedding/index.html create mode 100644 apps/site/learn/embedding/model.mjs create mode 100644 apps/site/learn/embedding/model.test.mjs create mode 100644 apps/site/learn/embedding/stage.mjs create mode 100644 apps/site/learn/embedding/style.css create mode 100644 docs/ai-learning/releases/2026-10-27-embedding.md create mode 100644 evals/browser/embedding-lifecycle.html diff --git a/apps/course/catalog.json b/apps/course/catalog.json index 4cc5722..4501e28 100644 --- a/apps/course/catalog.json +++ b/apps/course/catalog.json @@ -15,9 +15,9 @@ "id": "embedding", "order": 2, "title": "编号怎样变成向量?", - "outcome": "跟踪查表得到的向量,并比较方向、距离与其教学边界。", - "status": "planned", - "route": null, + "outcome": "用编号定位词表的一行,取出向量;再用坐标比较距离与方向,区分编号与几何关系。", + "status": "reviewed", + "route": "/learn/embedding/", "published": false }, { diff --git a/apps/site/learn/embedding/LESSON.md b/apps/site/learn/embedding/LESSON.md new file mode 100644 index 0000000..cbf4a5b --- /dev/null +++ b/apps/site/learn/embedding/LESSON.md @@ -0,0 +1,43 @@ +# 编号怎样变成向量? + +Core question: Given a fixed embedding parameter table E, which vector does Token ID i retrieve? + +## Teaching sequence + +1. Identify the input Token and its integer address. The default car / ID 1 intentionally contrasts with cat / ID 0. +2. Select exactly one row of the four-row, two-column table, with zero-based indexing. +3. Copy both components unchanged into the output. The original table row remains present: lookup does not consume it or multiply by the index. +4. Construct the vector on equal-scale coordinates. The path first places x₁, then x₂. This is a drawing aid, not a model's computational execution trace. +5. Compare endpoints and directions with fixed cat [2,1]. Car is distance 4, cosine −0.6; dog is distance √0.08 ≈0.283, cosine ≈0.992. ID proximity is not vector proximity. +6. Keep the same table and switch only input ID. Predict E[2], then verify it without changing the teaching table. + +## Review from the learner's perspective + +The input ID and output vector are visible before starting; a single bubble points at the current addressed row, copied row or plotted endpoint. Rows/words retain their colors. No wheel or mechanical metaphor is reused. Pausing restores configuration; a control never silently changes the table. The source boundary is on the stage and explained in the inspectable model dialog. + +Potential misconception: hand-authored cat/dog coordinates look like measured semantic knowledge. Mitigation: always-visible teaching-table notice, explicit untrained data, unnamed axes and statements that distance does not guarantee semantic truth. The scene explains static token lookup, not contextual embeddings or sentence embeddings. It does not assert measured learning gains. The prediction check is a teaching review aid, not a learner study. + +## Sources checked 2026-10-27 + +- PyTorch nn.Embedding official documentation: https://docs.pytorch.org/docs/stable/generated/torch.nn.Embedding.html — fixed dictionary lookup indexed by integers; weight shape (num_embeddings, embedding_dim). No padding/max_norm/training behaviors are simulated here. +- PyTorch word embeddings tutorial: https://docs.pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html — dense vector representation and cosine comparison. Numeric fixtures are original authored teaching values, not quotations or real model output. + +## Validation record + +Numerical/model checks include independent golden rows, invalid index rejection, unchanged source table after output mutation, golden distances/cosines, symmetry/self identity/positive scaling, and every 0.05-second sample forward and backward. Actual browser and deployment evidence will be recorded after inspection. Do not read candidate status as a production release. + + +### Browser observations (2026-10-27) + +Environment: managed Chrome, desktop 1363×936; same browser with a 390×844 iframe viewport (not a physical phone). Browser actions used actual controls, not app-state injection. + +- Full default playback: progressed through copying and coordinates to 44/44, play state false, configuration restored; end state car distance 4.000, cosine −0.600. +- Backward timeline 44→16, keyboard Home→0, next/back step controls, pause with Escape and resume were observed. Values/phase changed together, no accumulated future-state traces. +- ID switch at final state: dog [1.8,1.2], distance .283 and cosine .992; book [−1,−2], distance 4.243 and cosine −.800. Same table retained. +- Wrong quiz answer explained why ID multiplication is incorrect; correct answer and verify returned ID 2 and its unchanged vector in the diagram, with focus restored to the challenge trigger. +- Narrow-screen table, negative-coordinate labels, plot, formulas and dialog inspected. Initial defects: output caption touched last row, and annotation line crossed table/axis labels. Repaired by spacing output and routing the annotation through an empty side corridor; inspected again at desktop and narrow width. +- Reduced-motion manual toggle was exercised at t=13.1: copied row appeared at the stable destination immediately, with ordinary time/steps retained. OS preference initializes the same flag; actual OS preference switching not independently exercised. +- `evals/browser/embedding-lifecycle.html` dispatched controlled visibilitychange events on the real app document. Hidden event paused at ~.05s, restored configuration; visible event did not auto-play or jump time. Cloud-created tabs stayed visible to their document, so this is event-path testing, not a claim of observed OS tab suspension. +- CSS/inert: configuration left the accessibility tree during playback; focus remained on persistent pause. The stage and timeline continued to reflect one sampled state. + +Remaining: no physical-phone, screen-reader-device, other-browser-engine or empirical learner study. No claim of proven learning gain, production model embeddings or autostereoscopic hardware. Branch-preview and production verification are recorded in the release log; candidate source alone is not publication. diff --git a/apps/site/learn/embedding/SCENE_SPEC.json b/apps/site/learn/embedding/SCENE_SPEC.json new file mode 100644 index 0000000..fdfae7e --- /dev/null +++ b/apps/site/learn/embedding/SCENE_SPEC.json @@ -0,0 +1,17 @@ +{ + "id": "embedding", + "question": "一个 Token 编号如何从固定表中取出向量?", + "audience": "已看过 Token 编号、初识坐标的学习者", + "durationSeconds": 44, + "inputs": {"id": {"integer": true, "min": 0, "max": 3, "default": 1}}, + "objects": ["输入 Token 与 ID", "4×2 参数表 E", "被索引的整行", "输出向量", "等尺度二维坐标", "cat 参照向量"], + "relationships": ["e = E[id]", "horizontal = e[0]", "vertical = e[1]", "distance = Euclidean(e, E[0])", "cosine = dot(e,E[0]) / (norm(e)*norm(E[0]))"], + "table": [[2,1],[-2,1],[1.8,1.2],[-1,-2]], + "tokens": ["cat","car","dog","book"], + "mapping": {"color": "Token identity across input/table/output/plot", "axes": "both −3…3, equal units", "tableCellWidth": "layout only, not numerical magnitude", "depth": "decorative bevel, no quantitative perspective"}, + "phases": [{"start":0,"name":"input index"},{"start":6,"name":"select row"},{"start":13,"name":"copy unchanged components"},{"start":22,"name":"construct coordinates"},{"start":30,"name":"compare geometry"},{"start":38,"name":"controlled experiment"}], + "experiment": "Keep E fixed; choose ID 1 car then ID 2 dog and compare with ID 0 cat. ID adjacency does not imply vector proximity.", + "transfer": "Predict E[2]; explain row lookup versus duplicating/multiplying ID; verify on same diagram.", + "sources": ["https://docs.pytorch.org/docs/stable/generated/torch.nn.Embedding.html", "https://docs.pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html"], + "simplifications": ["Authored 2D table, not trained or extracted from a model", "No tokenizer executed; IDs are unique to this teaching table", "No position processing, context, attention, training or inference", "Axes are coordinates with no named semantic interpretation", "Geometric similarity does not establish semantic truth", "SVG depth on an ordinary display, not binocular autostereoscopy"] +} diff --git a/apps/site/learn/embedding/app.mjs b/apps/site/learn/embedding/app.mjs new file mode 100644 index 0000000..0d50257 --- /dev/null +++ b/apps/site/learn/embedding/app.mjs @@ -0,0 +1,25 @@ +import {TOKENS,STARTS,DURATION,sample,narration} from './model.mjs'; +import {createStage} from './stage.mjs'; +const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)],motion=matchMedia('(prefers-reduced-motion: reduce)'); +let reduced=motion.matches; +const stage=createStage($('#artwork'),{reducedMotion:()=>reduced}); +let id=1,time=0,playing=false,raf=0,last=0,lastCopy='',dead=false; +const names=['输入编号','定位一行','取出向量','两个坐标','比较几何','反事实实验']; +function focus(on){$('#app').classList.toggle('is-playing',on);$$('.secondary').forEach(x=>{x.inert=on;x.setAttribute('aria-hidden',String(on));});} +function anchor(){const box=$('.exhibit').getBoundingClientRect(),bubble=$('#callout').getBoundingClientRect(),art=$('#artwork');art.style.top=Math.max(185,bubble.bottom-box.top+13)+'px';const a=art.getBoundingClientRect(),point=stage.projectAnchor();if(!point)return;const x=a.left-box.left+point.x*a.width,y=a.top-box.top+point.y*a.height,cx=Math.max(bubble.width/2+17,Math.min(box.width-bubble.width/2-17,x));$('#callout').style.left=cx+'px';$('#leader').setAttribute('viewBox',`0 0 ${box.width} ${box.height}`);const sy=bubble.bottom-box.top;const side=a.left-box.left+point.bendX*a.width,start=cx-bubble.width/2+10;$('#leader-line').setAttribute('d',point.routeLeft?`M${start} ${sy}C${side} ${sy+12},${side} ${sy+12},${side} ${sy+27}V${y-8}Q${side} ${y},${side+8} ${y}H${x-8}`:`M${cx} ${sy}C${cx} ${sy+20},${x} ${y-25},${x} ${y-8}`);$('#leader-dot').setAttribute('cx',x);$('#leader-dot').setAttribute('cy',y);} +function render(){if(dead)return;const f=sample(time,{id}),c=narration(f),key=JSON.stringify(c);stage.render(f);if(key!==lastCopy){lastCopy=key;$('#copy-eyebrow').textContent=c.eyebrow;$('#copy-title').textContent=c.title;$('#copy-body').textContent=c.body;}$('#phase-label').textContent=`0${f.phase+1} / 06 · ${names[f.phase]}`;$('#counts').textContent=`ID ${id} · ${TOKENS[id]} · 2 维`;$$('[data-id]').forEach(b=>b.setAttribute('aria-pressed',String(+b.dataset.id===id)));$('#timeline').value=time;$('#timeline').style.setProperty('--fill',`${time/DURATION*100}%`);$('#time').textContent=`00:${String(Math.floor(time)).padStart(2,'0')} / 00:44`;$('#play-label').textContent=playing?'暂停':time>=DURATION?'再看一遍':time?'继续讲解':'播放讲解';$('#play-icon').textContent=playing?'Ⅱ':'▶';$('#play').setAttribute('aria-pressed',String(playing));$('#back').disabled=time===0;$('#next').disabled=time===DURATION;$('#play-note').textContent=playing?'配置已收起 · 空格暂停':'六个步骤 · 从编号到向量';$('#challenge').hidden=timeseek(+e.target.value);$('#back').onclick=()=>seek(STARTS.filter(t=>tseek(STARTS.find(t=>t>time+.1)??DURATION);$('#reset').onclick=()=>{stop();time=0;id=1;render();}; +$$('[data-id]').forEach(b=>b.onclick=()=>{stop();id=+b.dataset.id;render();});$('#experiment').onclick=()=>{seek(38);$('#app [data-id="1"]').focus();}; +function setReduced(value){reduced=value;$('#reduce-motion').checked=value;$('#app').classList.toggle('reduce-motion',value);render();} +$('#reduce-motion').onchange=e=>setReduced(e.target.checked);motion.addEventListener('change',e=>setReduced(e.matches)); +function info(){stop();$('#info').showModal();}$('#about').onclick=info;$('#show-table').onclick=info;$('#close-info').onclick=()=>$('#info').close(); +$('#challenge').onclick=()=>{stop();$('#feedback').textContent='';$('#quiz-try').hidden=true;$$('[data-answer]').forEach(b=>b.removeAttribute('aria-pressed'));$('#quiz').showModal();}; +$$('[data-answer]').forEach(b=>b.onclick=()=>{$$('[data-answer]').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));$('#feedback').textContent=(b.dataset.answer==='row'?'预测正确。':'输出是 [1.8, 1.2]。')+'ID 2 只定位第 2 行,不重复编号,也不乘 2。坐标 (1.8, 1.2) 离 cat 的 (2, 1) 仅 0.283;两者 ID 不相邻。这个静态查表还没有处理上下文。';$('#quiz-try').hidden=false;}); +$('#quiz-try').onclick=()=>{$('#quiz').close();id=2;time=44;render();};$('#close-quiz').onclick=()=>$('#quiz').close(); +document.addEventListener('visibilitychange',()=>{if(document.hidden&&playing)stop();});document.addEventListener('keydown',e=>{if($('#info').open||$('#quiz').open)return;if(e.key==='Escape')stop();if(e.code==='Space'&&!['INPUT','BUTTON','A'].includes(e.target.tagName)){e.preventDefault();play();}}); +const observer=new ResizeObserver(()=>{stage.resize();anchor();});observer.observe($('#artwork'));$('.exhibit').addEventListener('transitionend',anchor);window.addEventListener('pagehide',e=>{stop();if(!e.persisted){dead=true;observer.disconnect();stage.dispose();}});setReduced(reduced); diff --git a/apps/site/learn/embedding/index.html b/apps/site/learn/embedding/index.html new file mode 100644 index 0000000..da7ba94 --- /dev/null +++ b/apps/site/learn/embedding/index.html @@ -0,0 +1,3 @@ +编号怎样变成向量? · Explain3D
e· Explain3DAI 原理课堂 / 02

AN ADDRESS BECOMES A VECTOR

编号,怎样变成向量?

一个索引,取出一整行;两个数,定位一个向量。

固定教学表 · 只改变输入 ID

手写二维教学表 · 静态词元向量 · 非真实模型
六个步骤 · 从编号到向量00:00 / 00:44
Explain3D · AI 原理课堂ID 是索引 · E ∈ ℝ⁴ˣ²
+

ONE ROW, TWO COORDINATES

查表不是“把编号算成词义”。

输入是整数索引;输出是 Embedding 参数矩阵中对应的整行。这里从 0 开始编号,E 的形状为 4 × 2。固定表下,同一个 ID 每次得到同一向量。后续的位置处理、Attention 和网络层会进一步改变表示,本页不演示它们。

0 · cat → [ 2.0, 1.0]
1 · car → [-2.0, 1.0]
2 · dog → [ 1.8, 1.2]
3 · book → [-1.0, -2.0]

这些 Token、ID 和数值全部手写,只为看清查表和几何关系;没有训练或调用真实模型。真实模型通常从训练中学习这些参数,维度也通常更高。本页的 x₁、x₂ 没有“动物”“速度”等预设语义;距离和余弦也不能保证真实世界中的词义关系。

e = E[id]
距离 = √((a₁ − b₁)² + (a₂ − b₂)²)
方向余弦 = (a₁b₁ + a₂b₂) / (‖a‖ ‖b‖)

图中坐标轴同尺度,范围均为 −3…3;箭头指向真实坐标。颜色只标识同一个 Token;表格单元格、输入卡片的宽度不表示数值大小。查表没有混合其他 Token,也没有根据上下文消除歧义。

PyTorch Embedding 官方说明 ↗ · 词向量与相似度官方教程 ↗

+

PREDICT · TRY · EXPLAIN

ID 变成 2,会取出什么?

保持同一张表。已知 E 的第 2 行是 [1.8, 1.2]。选择 dog(ID 2),静态 Embedding 输出是什么?

\ No newline at end of file diff --git a/apps/site/learn/embedding/model.mjs b/apps/site/learn/embedding/model.mjs new file mode 100644 index 0000000..baa62ef --- /dev/null +++ b/apps/site/learn/embedding/model.mjs @@ -0,0 +1,15 @@ +export const TOKENS=Object.freeze(['cat','car','dog','book']); +export const COLORS=Object.freeze(['#6b8fc6','#c58b73','#73a995','#9b84ca']); +export const TABLE=Object.freeze([[2,1],[-2,1],[1.8,1.2],[-1,-2]].map(Object.freeze)); +export const DURATION=44,STARTS=[0,6,13,22,30,38]; +export function lookup(id){if(!Number.isInteger(id)||id<0||id>=TABLE.length)throw new RangeError('Token ID must be 0…3');return [...TABLE[id]];} +export function compare(a,b){const dot=a[0]*b[0]+a[1]*b[1],normA=Math.hypot(...a),normB=Math.hypot(...b);if(!normA||!normB)throw new RangeError('Cosine undefined for zero vector');return {dot,normA,normB,cosine:dot/normA/normB,distance:Math.hypot(a[0]-b[0],a[1]-b[1])};} +export function sample(t,{id=1}={}){const vector=lookup(id);t=Number.isFinite(t)?Math.max(0,Math.min(DURATION,t)):0;const phase=STARTS.findLastIndex(x=>t>=x),end=STARTS[phase+1]??DURATION;return {time:t,phase,progress:(t-STARTS[phase])/(end-STARTS[phase]),id,token:TOKENS[id],vector,reference:lookup(0),...compare(vector,lookup(0))};} +export function narration(f){const v='['+f.vector.join(', ')+']';return [ + {eyebrow:'01 · INPUT',title:'编号是一张地址标签',body:`输入是 ${f.token} 的 ID ${f.id}。它只决定取哪一行,不是词义大小,也不是向量的数值。`}, + {eyebrow:'02 · LOOKUP',title:`找到 E 的第 ${f.id} 行`,body:`从 0 开始编号。E 有 4 行、每行 2 个数;${f.token} 对应的这一行是 ${v}。表中数值已预先确定。`}, + {eyebrow:'03 · COPY',title:'整行取出,不乘编号',body:`e = E[${f.id}] = ${v}。两个分量原样成为输出;查表既不相加,也不把编号 ${f.id} 乘到这一行上。`}, + {eyebrow:'04 · COORDINATES',title:'两个数,定位一个向量',body:`从原点出发:横坐标 x₁ = ${f.vector[0]},纵坐标 x₂ = ${f.vector[1]}。两条轴同尺度;负数向左或向下。坐标轴没有预设的语义名称。`}, + {eyebrow:'05 · COMPARE',title:'相近的是向量,不是编号',body:`与 cat 的 [2, 1] 比较:距离 ${f.distance.toFixed(3)},方向余弦 ${f.cosine.toFixed(3)}。这些数由坐标计算,不能单凭 ID 的差值推断。`}, + {eyebrow:'06 · YOUR EXPERIMENT',title:'换一个地址,结果会怎样?',body:'暂停后依次选 car 与 dog,对比它们和 cat 的关系。ID 1 比 ID 2 更靠近 0,但此表中 dog 的向量离 cat 更近。'} + ][f.phase];} diff --git a/apps/site/learn/embedding/model.test.mjs b/apps/site/learn/embedding/model.test.mjs new file mode 100644 index 0000000..b1cf614 --- /dev/null +++ b/apps/site/learn/embedding/model.test.mjs @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict'; +import {lookup,compare,sample,DURATION,TABLE} from './model.mjs'; +const close=(a,b)=>assert(Math.abs(a-b)<1e-12,`${a} vs ${b}`); +// Gold outputs are independent authored fixtures, not recomputed from TABLE. +assert.deepEqual([0,1,2,3].map(lookup),[[2,1],[-2,1],[1.8,1.2],[-1,-2]]); +const a=lookup(1);a[0]=99;assert.deepEqual(lookup(1),[-2,1]); +for(const id of [-1,4,1.1,NaN,Infinity,'2',null])assert.throws(()=>lookup(id),RangeError); +close(compare([2,1],[-2,1]).distance,4);close(compare([2,1],[-2,1]).cosine,-.6); +close(compare([1.8,1.2],[2,1]).distance,Math.sqrt(.08));close(compare([-1,-2],[2,1]).cosine,-.8); +assert(compare(lookup(2),lookup(0)).distance=-1-1e-12&&ab.cosine<=1+1e-12);close(compare(v,v).cosine,1);close(compare(v,v).distance,0);close(compare(v.map(x=>2*x),w).cosine,ab.cosine);} +assert.throws(()=>compare([0,0],[1,1]),RangeError); +for(let id=0;id<4;id++){const frames=[];for(let t=0;t<=DURATION;t+=.05){const f=sample(t,{id});assert.deepEqual(f.vector,lookup(id));assert(f.phase>=0&&f.phase<=5);assert(f.progress>=0&&f.progress<=1+1e-12);frames.push([t,f]);}for(const[t,f]of frames.reverse())assert.deepEqual(sample(t,{id}),f);} +assert.equal(sample(-1).time,0);assert.equal(sample(Infinity).time,0);assert.equal(sample(100).time,DURATION);assert.equal(sample(DURATION).progress,1); +console.log('Embedding: golden rows, index boundaries, immutable lookup, exact distance/cosine, scale/symmetry invariants and deterministic forward/backward sampling passed.'); diff --git a/apps/site/learn/embedding/stage.mjs b/apps/site/learn/embedding/stage.mjs new file mode 100644 index 0000000..72e5c94 --- /dev/null +++ b/apps/site/learn/embedding/stage.mjs @@ -0,0 +1,43 @@ +import {TOKENS,TABLE,COLORS} from './model.mjs'; +const NS='http://www.w3.org/2000/svg'; +const node=(tag,attrs,parent)=>{const n=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))n.setAttribute(k,v);parent.append(n);return n;}; +export function createStage(host,{reducedMotion=()=>false}={}){ + const svg=node('svg',{role:'img','viewBox':'0 0 1000 360'},host);svg.style.cssText='width:100%;height:100%;overflow:visible';let anchor=[0,0],last,key=''; + function render(f){last=f;const mobile=host.clientWidth<600,W=mobile?500:1000,H=360,p=reducedMotion()?1:f.progress,k=[f.id,f.phase,mobile,reducedMotion(),f.phase===2||f.phase===3?Math.round(p*180):''].join('|');if(k===key)return;key=k;svg.replaceChildren();svg.setAttribute('viewBox',`0 0 ${W} ${H}`);const color=COLORS[f.id]; + const text=(x,y,s,size=18,fill='#777487',align='middle')=>{const n=node('text',{x,y,'text-anchor':align,fill,'font-size':size,'font-family':'ui-monospace,monospace'},svg);n.textContent=s;return n;}; + const line=(x1,y1,x2,y2,c='#d8d5dc',dash='')=>node('line',{x1,y1,x2,y2,stroke:c,'stroke-width':1,'stroke-dasharray':dash},svg); + const rect=(x,y,w,h,c='#fffdfa',stroke='#ddd7df',r=6)=>node('rect',{x,y,width:w,height:h,rx:r,fill:c,stroke,'stroke-width':.8},svg); + const plate=(x,y,w,h,c)=>{node('path',{d:`M${x} ${y}l6 -5h${w}l-6 5Z`,fill:'#fff',stroke:'#d6d6df','stroke-width':.7},svg);rect(x,y,w,h,'#fdfbfa',c);line(x+4,y+3,x+w-4,y+3,'#fff');}; + const arrow=(x,y,a,b,c,width=2)=>{line(x,y,a,b,c);const ang=Math.atan2(b-y,a-x),sz=7;node('path',{d:`M${a} ${b}L${a-sz*Math.cos(ang-.42)} ${b-sz*Math.sin(ang-.42)}M${a} ${b}L${a-sz*Math.cos(ang+.42)} ${b-sz*Math.sin(ang+.42)}`,stroke:c,fill:'none','stroke-width':width},svg);}; + if(f.phase<3){ + const tx=mobile?83:160,ty=mobile?78:70,tw=mobile?334:350,rh=38,ox=mobile?130:660,oy=mobile?300:138,ow=mobile?240:240; + plate(mobile?145:170,0,210,34,color);text(mobile?250:275,24,`${f.token} · ID ${f.id}`,21,color); + text(tx+tw/2,ty-14,'E · 4 行 × 2 列',16,'#9a8aa5'); + text(tx+12,ty+13,'ID / Token',12,'#a798ac','start');text(tx+tw-105,ty+13,'x₁',14);text(tx+tw-40,ty+13,'x₂',14); + TABLE.forEach((v,i)=>{const y=ty+24+i*rh,hot=i===f.id&&f.phase>=1,c=COLORS[i];plate(tx,y,tw,rh-3,hot?c:'#ddd7df');if(hot)node('rect',{x:tx+1,y:y+1,width:tw-2,height:rh-5,rx:5,fill:c,'fill-opacity':.13},svg);node('circle',{cx:tx+13,cy:y+17,r:3,fill:c},svg);text(tx+28,y+23,`${i} ${TOKENS[i]}`,19,c,'start');text(tx+tw-105,y+23,v[0].toFixed(1),20,hot?c:'#88818e');text(tx+tw-40,y+23,v[1].toFixed(1),20,hot?c:'#88818e');}); + const sy=ty+24+f.id*rh; + if(f.phase>=1){const bx=tx-13;node('path',{d:`M${bx+6} ${sy+1}H${bx}v${rh-5}h6`,stroke:color,fill:'none','stroke-width':1.5},svg);anchor=[tx-8,sy+17];}else anchor=[mobile?145:170,28]; + if(!mobile){line(tx+tw+15,sy+17,ox-18,oy+25,'#d7cbdc','3 4');} + rect(ox-8,oy-8,ow+16,66,'#f3f0f5','#ddd5e3');text(ox+ow/2,oy-20,'输出向量 e = E[id]',15,'#9a8aa5'); + if(f.phase<2)text(ox+ow/2,oy+30,'[ ?, ? ]',23,'#b6aabd'); + if(f.phase===2){const u=Math.min(1,p*1.65),q=u*u*(3-2*u),sx=tx+tw-147,sw=130,dx=ox+(ow-180)/2,x=sx+(dx-sx)*q,y=sy+(oy-sy)*q,w=sw+(180-sw)*q;rect(x,y,w,rh-3,'#fffdfa',color);text(x+w/2,y+23,`[${f.vector.map(n=>n.toFixed(1)).join(', ')}]`,20,color);anchor=mobile?[ox-15,oy+17]:[x-5,y+17];if(!mobile)text(780,250,'整行取出 · 数值保持不变',17,'#9a8aa5');} + }else{ + // Exact front-plane coordinates. The same scale is used on both axes. + const cx=mobile?245:500,cy=176,unit=41,xy=v=>[cx+v[0]*unit,cy-v[1]*unit]; + for(let n=-3;n<=3;n++){line(cx-3*unit,cy+n*unit,cx+3*unit,cy+n*unit,'#efebef');line(cx+n*unit,cy-3*unit,cx+n*unit,cy+3*unit,'#efebef');if(n){text(cx+n*unit,cy+17,String(n),12,'#aaa0af');text(cx-13,cy-n*unit+4,String(n),12,'#aaa0af');}} + arrow(cx-135,cy,cx+143,cy,'#b1a8b8',1);arrow(cx,cy+135,cx,cy-143,'#b1a8b8',1);text(cx+160,cy+5,'x₁',17);text(cx+1,cy-154,'x₂',17);text(cx-12,cy+17,'0',12,'#aaa0af'); + const end=xy(f.vector),reference=xy(f.reference); + if(f.phase===3){const u=Math.min(1,p*1.7),h=Math.min(1,u*2),v=Math.max(0,u*2-1),px=cx+(end[0]-cx)*h,py=cy+(end[1]-cy)*v;line(cx,cy,end[0],cy,color,'3 3');line(end[0],cy,end[0],end[1],color,'3 3');arrow(cx,cy,px,py,color,2);node('circle',{cx:px,cy:py,r:4,fill:color},svg);anchor=[end[0]+8,end[1]];text(mobile?250:W/2,334,`x₁ = ${f.vector[0]} x₂ = ${f.vector[1]}`,20,color);} + else{ + if(f.id!==0){arrow(cx,cy,...reference,COLORS[0],2);node('circle',{cx:reference[0],cy:reference[1],r:3.5,fill:COLORS[0]},svg);text(reference[0]+9,reference[1]-12,'cat [2, 1]',15,COLORS[0],'start');line(...reference,...end,'#c8b5d4','4 4');} + arrow(cx,cy,...end,color,2);node('circle',{cx:end[0],cy:end[1],r:4,fill:color},svg);text(end[0]+(f.vector[0]<0?-10:10),end[1]+(f.id===2?22:-14),`${f.token} [${f.vector.join(', ')}]`,16,color,f.vector[0]<0?'end':'start');anchor=[end[0]+(f.vector[0]<0?-8:8),end[1]]; + const bx=mobile?40:85,by=mobile?317:140; + if(!mobile){text(bx,by,'对照 cat · [2, 1]',17,COLORS[0],'start');text(bx,by+35,`距离 ${f.distance.toFixed(3)}`,21,'#8a6fa1','start');text(bx,by+66,`余弦 ${f.cosine.toFixed(3)}`,21,'#8a6fa1','start');text(bx,by+100,'距离越小,端点越接近',12,'#9a8aa5','start');text(bx,by+122,'余弦越接近 1,方向越一致',12,'#9a8aa5','start');} + else{text(W/2,by,`与 cat:距离 ${f.distance.toFixed(3)} · 余弦 ${f.cosine.toFixed(3)}`,17,'#8a6fa1');text(W/2,by+26,'虚线看距离 · 余弦越近 1,方向越一致',12,'#9a8aa5');} + } + if(!mobile){text(800,150,'e = E['+f.id+']',23,color);text(800,185,'['+f.vector.join(', ')+']',26,color);text(800,219,'输入 ID → 一行参数',13,'#9a8aa5');} + } + svg.setAttribute('aria-label',`${f.token} 的 ID 为 ${f.id},阶段 ${f.phase+1}。查表向量 [${f.vector.join(', ')}]。${f.phase>=4?`与 cat 的距离 ${f.distance.toFixed(3)},方向余弦 ${f.cosine.toFixed(3)}。`:''}`); + } + return {render,projectAnchor(){const m=svg.getScreenCTM(),r=host.getBoundingClientRect();if(!m||!r.width||!r.height)return null;const point=svg.createSVGPoint();point.x=anchor[0];point.y=anchor[1];const p=point.matrixTransform(m);return{x:(p.x-r.left)/r.width,y:(p.y-r.top)/r.height,visible:true,routeLeft:(host.clientWidth<600&&last.phase<3)||(last.phase>=3&&last.vector[0]<0),bendX:((host.clientWidth<600?20:300)*m.a+m.e-r.left)/r.width};},resize(){key='';if(last)render(last);},dispose(){svg.remove();}}; +} diff --git a/apps/site/learn/embedding/style.css b/apps/site/learn/embedding/style.css new file mode 100644 index 0000000..9c4a5da --- /dev/null +++ b/apps/site/learn/embedding/style.css @@ -0,0 +1,20 @@ +:root{--paper:#f7f5ef;--ink:#343743;--muted:#817b87;--accent:#8066a6;--line:#e8e3da;font-family:Inter,'PingFang SC','Microsoft YaHei',system-ui,sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}*{box-sizing:border-box}body{margin:0}button,input,a{-webkit-tap-highlight-color:transparent}button{font:inherit;cursor:pointer;border:0}button:focus-visible,input:focus-visible,a:focus-visible{outline:2px solid #8e74b5;outline-offset:4px}button:disabled{opacity:.3;cursor:default}a{color:inherit;text-decoration:none}.app{height:100svh;min-height:790px;max-width:1600px;margin:auto;padding:0 40px;display:flex;flex-direction:column}header{height:58px;display:flex;gap:24px;align-items:center;flex-shrink:0}.brand{font-weight:700;font-size:19px;letter-spacing:-.6px}.brand i{font:italic bold 28px Georgia,serif;color:var(--accent);margin-right:4px}header>span{font-size:10px;letter-spacing:1px;color:#928a98}header>.quiet{margin-left:auto}.quiet{background:transparent;color:#82768b;border-radius:7px;font-size:11px;padding:8px 10px;white-space:nowrap}.quiet:hover{color:#554362;background:#eee8ee}.intro{display:flex;align-items:flex-end;justify-content:space-between;max-height:112px;padding:10px 0 23px;overflow:hidden;flex-shrink:0}.eyebrow{font-size:9px;letter-spacing:2px;font-weight:600;color:#8e7c9b;margin:0 0 10px}h1{margin:0;font-size:clamp(27px,2.6vw,38px);font-weight:650;letter-spacing:-1.2px;line-height:1.35}h1 span{color:#7c6c8e}.intro-copy{font-size:12px;color:#817788;margin:0 0 4px}.controls{height:60px;max-height:60px;display:flex;align-items:center;gap:17px;border:1px solid #e6e0d7;background:#efede6;padding:0 18px;border-radius:13px;margin-bottom:17px;flex-shrink:0;overflow:hidden}.temperature{display:flex;align-items:center;gap:20px}.temperature label{font-size:11px;white-space:nowrap;color:#756c7c}.temperature output{font-family:ui-monospace,monospace;color:var(--accent);margin-left:12px;font-size:12px}input[type=range]{appearance:none;background:transparent;height:24px;margin:0;cursor:pointer}input[type=range]::-webkit-slider-runnable-track{height:3px;border-radius:4px;background:linear-gradient(to right,#9b85b9 var(--fill,0%),#d9d1df var(--fill,0%))}input[type=range]::-webkit-slider-thumb{appearance:none;width:12px;height:12px;border-radius:50%;background:#8b73ac;margin-top:-4.5px;box-shadow:0 0 0 3px #f7f5ef}input[type=range]::-moz-range-track{height:3px;background:#d9d1df}input[type=range]::-moz-range-progress{height:3px;background:#9b85b9}input[type=range]::-moz-range-thumb{height:12px;width:12px;border-radius:50%;border:0;background:#8b73ac}#temperature{width:155px}.presets{display:flex;gap:3px}.presets button{font-size:10px;padding:8px 11px;background:none;color:#7f7187;border-radius:6px;white-space:nowrap}.presets button[aria-pressed=true],.presets button:hover{background:#fffcf8;color:#6f5387}.divider{height:22px;width:1px;background:#ded7df;margin-left:auto}#new-draw span{margin-right:4px;font-size:15px}.experiment{font-size:11px;padding:10px 14px;background:#8b749e;color:white;border-radius:7px;white-space:nowrap}.experiment:hover{background:#78618e}.exhibit{position:relative;flex:1;min-height:420px;background:#fffdfa;border:1px solid #e9e4db;border-radius:18px;overflow:hidden;isolation:isolate}.exhibit-meta{position:absolute;top:21px;left:25px;right:25px;display:flex;justify-content:space-between;color:#84798c;font-size:10px;letter-spacing:.6px;z-index:2}#live-temperature{font-family:ui-monospace,monospace;color:#8a73a2}.context{position:absolute;top:65px;left:27px;z-index:2;max-height:80px;overflow:hidden}.context>span{font-size:9px;color:#94899c}.context p{font:14px Georgia,serif;color:#7c7186;margin:9px 0}.context b{color:#a28dbb}.callout{position:absolute;top:56px;width:380px;left:50%;transform:translateX(-50%);padding:15px 19px 11px;border:1px solid #e7dfee;background:#fffffff7;border-radius:13px;box-shadow:0 8px 26px #88779905;z-index:4}.callout>span{font-size:9px;letter-spacing:.8px;color:#9a80b4}.callout h2{font-size:18px;font-weight:650;line-height:1.5;letter-spacing:-.25px;margin:7px 0}.callout p{font-size:12px;line-height:1.8;color:#766a7e;margin:0}.calculation{font-size:10px;color:#9579ac;background:transparent;padding:8px 0 0}.calculation:hover{color:#674280}.leader{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;color:#af97c5}.leader path{fill:none;stroke:currentColor;stroke-width:1.1}.leader circle{fill:#fffdfa;stroke:currentColor;stroke-width:1.1}.artwork{position:absolute;left:50%;bottom:39px;transform:translateX(-50%);width:min(96%,1160px);height:calc(100% - 208px);display:flex;align-items:center;justify-content:center;transition:transform .4s}.exhibit.is-detailed .artwork{transform:translateX(-50%) scale(1.3);transform-origin:center 70%}.stage-bottom{position:absolute;bottom:13px;left:25px;right:17px;display:flex;align-items:center;justify-content:space-between;z-index:5;color:#8a7f92;font-size:9px}.stage-bottom .quiet{font-size:10px}.transport{height:70px;display:flex;gap:18px;align-items:center;flex-shrink:0}.play{height:42px;width:134px;display:flex;align-items:center;justify-content:center;gap:11px;border-radius:9px;background:#3c3543;color:#fff;font-size:11px;flex-shrink:0}.play:hover{background:#62516f}#play-icon{font-size:10px}.track{flex:1;min-width:0}.track input{display:block;width:100%}.track>div{display:flex;justify-content:space-between;color:#8c7d98;font-size:9px;font-variant-numeric:tabular-nums}footer{height:30px;max-height:30px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;overflow:hidden;font-size:9px;color:#a497ac}.secondary{transition:opacity .35s,max-height .6s cubic-bezier(.22,1,.36,1),padding .6s,margin .6s}.is-playing .secondary{max-height:0;opacity:0;padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0;pointer-events:none}.is-playing .controls{border-width:0}.is-playing .exhibit{border-color:transparent}.is-playing .calculation,.is-playing #detail{display:none}.is-playing .artwork{height:calc(100% - 185px);bottom:35px}.is-playing .stage-bottom{bottom:19px}.is-playing .callout{padding-bottom:15px} +dialog{border:1px solid #e5dce9;background:#fffdf8;border-radius:18px;max-width:720px;width:94%;max-height:88svh;padding:34px;color:#4d4058;box-shadow:0 25px 100px #3e2c4829}dialog::backdrop{background:#34263e55;backdrop-filter:blur(4px)}dialog h2{font-size:25px;line-height:1.4;margin:0 0 16px;letter-spacing:-.6px}dialog p{font-size:13px;line-height:1.9;color:#81718d}dialog a{color:#8b6aa4;text-decoration:underline;text-underline-offset:3px}.close{position:absolute;right:16px;top:10px;background:none;color:#9883a6;font-size:26px}.fine{font-size:11px!important;color:#9786a2!important}.formula{font:14px/1.8 ui-monospace,monospace;background:#f0ebf3;border:1px solid #e4dce9;border-radius:8px;padding:12px 16px;margin:16px 0;color:#7b5e98;overflow-x:auto}.table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse;text-align:right;font-variant-numeric:tabular-nums;font-size:12px}th{font-size:10px;color:#95819f;font-weight:500;white-space:nowrap;padding:9px 10px;border-bottom:1px solid #e7dfeb}td{padding:14px 10px;border-bottom:1px solid #eee8f1;color:#6b567a}td:first-child,th:first-child{text-align:left}td:first-child i{width:7px;height:7px;display:inline-block;border-radius:50%;margin-right:7px}.calculation-total{font-size:12px;color:#79628a;text-align:right} +@media(max-width:1100px){.app{padding:0 24px}.intro-copy{display:none}.controls{gap:10px}.temperature{gap:12px}#temperature{width:120px}.presets button{padding:8px}.context{display:none}.divider{margin-left:auto}} +@media(max-width:700px){.app{padding:0 12px;min-height:790px}header{height:48px;gap:14px}.brand{font-size:17px}.brand i{font-size:25px}header>span{font-size:8px;letter-spacing:0}header>.quiet{font-size:9px;padding-right:0}.intro{padding:9px 0 18px;max-height:105px}h1{font-size:24px;line-height:1.45;letter-spacing:-.7px}.eyebrow{font-size:8px;margin-bottom:7px}.controls{height:98px;max-height:98px;display:grid;grid-template-columns:1fr auto;gap:8px 4px;padding:10px 12px;margin-bottom:11px}.temperature{grid-column:1/-1;grid-row:1;gap:17px}.temperature label{font-size:11px}#temperature{flex:1;width:auto}.presets{grid-row:2;gap:0}.presets button{font-size:9px;padding:7px 9px}.divider{display:none}#new-draw{display:none}.experiment{font-size:9px;padding:9px 10px}.exhibit{min-height:408px;border-radius:13px}.exhibit-meta{top:17px;left:17px;right:17px;font-size:9px}.callout{top:47px;width:calc(100% - 34px);max-width:380px;padding:13px 16px 10px}.callout h2{font-size:16px;line-height:1.5}.callout p{font-size:11px;line-height:1.7}.callout>span{font-size:8px}.calculation{font-size:9px}.artwork{width:100%;height:calc(100% - 192px);bottom:37px}.stage-bottom{left:17px;right:7px;bottom:10px;font-size:8px}.stage-bottom .quiet{font-size:9px}.transport{height:62px;gap:6px}.play{width:110px;height:39px;font-size:10px}.transport>.quiet{padding:6px}.track>div span:first-child{display:none}.track>div{justify-content:flex-end}.track input{height:23px}footer{height:27px;max-height:27px;font-size:8px}footer>span:last-child{max-width:150px;text-align:right}.is-playing{min-height:600px}.is-playing .artwork{height:calc(100% - 190px);bottom:40px}.is-playing .stage-bottom{bottom:16px}dialog{padding:30px 19px}dialog h2{font-size:22px}td,th{padding:10px 6px}td{font-size:11px}.formula{font-size:12px}} +@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}[hidden]{display:none!important} + +/* Reserve the callout before fitting the vector diagram. */ +.artwork,.is-playing .artwork{top:220px;bottom:35px;height:auto}.stage-actions{display:flex;align-items:center;gap:5px} +@media(max-width:700px){.artwork,.is-playing .artwork{top:205px;bottom:36px;height:auto}} + +.challenge-answers{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:20px 0}.challenge-answers button{background:#f1ebf5;color:#725389;border:1px solid #dfd3e8;border-radius:8px;padding:13px 8px;font:20px Georgia,serif}.challenge-answers button[aria-pressed=true]{background:#8b749e;color:white;border-color:#8b749e}#challenge-feedback:empty{display:none}#challenge-feedback{padding:15px;background:#f2f1ed;border-radius:8px;color:#665873;line-height:1.9}.stage-actions #challenge{color:#80609e} + +.is-playing #challenge{display:none} + +#rules{width:110px}.controls label{font-size:11px;white-space:nowrap;color:#83708e}.controls output{color:#8066a6;margin-left:8px}.is-playing #show-rules{display:none}.challenge-answers{grid-template-columns:repeat(3,1fr)}#feedback:empty{display:none}#feedback{background:#f0ebf3;border-radius:8px;padding:15px}.artwork svg{display:block} @media(max-width:700px){.controls{grid-template-columns:auto 1fr auto;height:102px;max-height:102px}.presets{grid-column:1/-1;grid-row:1}.controls label{grid-row:2;font-size:10px}#rules{grid-row:2;width:100%}#experiment{grid-row:2;font-size:9px}.presets button{padding:7px 12px}.stage-bottom{font-size:8px}} + +/* Embedding is an indexed table and an exact front-plane vector plot. */ +.controls{gap:14px}.controls .divider{margin-left:auto}.controls .hint{font-size:11px;color:#8a7d91}.presets button{padding:9px 14px}.challenge-answers{grid-template-columns:1fr}.challenge-answers button{font:13px ui-monospace,monospace;text-align:left;padding:14px}.artwork svg text{paint-order:stroke;stroke:#fffdfa;stroke-width:.4px;stroke-linejoin:round}@media(max-width:700px){.controls{display:flex;flex-wrap:wrap;height:83px;max-height:83px;gap:3px 9px;padding:8px 12px}.presets{width:100%;justify-content:space-between}.presets button{font-size:10px;padding:8px 12px}.controls .hint{font-size:9px;flex:1}.controls #experiment{font-size:9px;padding:7px 9px}.callout{min-height:132px}.exhibit{min-height:430px}.stage-bottom>span{max-width:220px;line-height:1.5}.artwork svg text{stroke-width:0}} + +.reduce-motion *{animation:none!important;transition:none!important}#reduce-motion{accent-color:#8b749e;margin-right:8px} diff --git a/docs/ai-learning/STATUS.md b/docs/ai-learning/STATUS.md index 12fb431..b08bdca 100644 --- a/docs/ai-learning/STATUS.md +++ b/docs/ai-learning/STATUS.md @@ -22,10 +22,15 @@ The user authorized ten scenes, an iteration every twelve hours, independent lea ## Remaining work -Eight scenes are planned, not implemented or advertised as playable. See `roadmap.json` for topics, learning outcomes and current source status. The existing experimental autoregression and roulette pages are not silently promoted to accepted lessons. +Seven scenes are planned, not implemented or advertised as playable. See `roadmap.json` for topics, learning outcomes and current source status. The existing experimental autoregression and roulette pages are not silently promoted to accepted lessons. Real learner sessions, physical phones, screen-reader devices and independent browser-engine coverage have not been completed. These limits do not prevent releasing a clearly labeled teaching sample, but they must not be represented as tested. ## Published lesson 01: tokenization (2 / 10 total) Route /learn/tokenization/. A 46-second fixed-rule character-level BPE teaching model shows input, base symbols, ranked adjacent merges, boundaries, vocabulary IDs and reversible decoding. Five supported English words and four rule versions are explicit; not a production tokenizer, no Chinese/space/byte-level support. Golden splits and IDs, span conservation, rule no-op, all version/time samples and decode round trips pass. Desktop and390×844 visual/quiz checks completed. The new inputunhug predicts two tokens and verifies[8,9]. This is developer teaching review, not measured learning gains. Published at https://explain.funagent.app/learn/tokenization/ . PR #2 head a5ce351fa1b6f9b7a28732d4aeb0655c1c30155a passed CI run40380762972 and a READY preview; preview HTML and app module returned200 with the slider fix. Merge sourcebb9c7d9eb0840a688bf3ef6e8f45471aa2c4e1f5 deployed READY asdpl_8jU8UL9yGByo1v3dQEPqpeBT8fUF. Production browser confirmed rules0→[2,6,1,5], rules3→[9,5], and quiz→unhug→[8,9]. Next: embedding. + + +## Reviewed candidate lesson 02: Embedding + +Route `/learn/embedding/`. One question: how an integer ID retrieves one row from a fixed parameter table. A four-row, two-dimensional authored example connects indexing, unchanged row copy, equal-scale coordinates and a controlled cat/car/dog comparison. Same ID is an address, not a semantic magnitude. Pausing restores configuration; one routed annotation avoids the table and negative-coordinate labels. Independent numerical/seek tests and desktop/narrow-width browser checks passed; reduced-motion manual toggle and controlled hidden-event path were exercised. This is teaching review, not learner-effect research. Production release is pending the branch CI/preview and main READY checks. Next after release: Attention. diff --git a/docs/ai-learning/releases/2026-10-27-embedding.md b/docs/ai-learning/releases/2026-10-27-embedding.md new file mode 100644 index 0000000..b0589a3 --- /dev/null +++ b/docs/ai-learning/releases/2026-10-27-embedding.md @@ -0,0 +1,7 @@ +# Embedding release candidate — 2026-10-27 + +Target: existing funAgent/explain3d → explain.funagent.app. New source `/learn/embedding/`; no mechanical templates changed. + +Checked: official PyTorch embedding lookup/shape and vector tutorial; independent golden rows, boundary IDs, immutability, distances, cosines, symmetry/scaling and deterministic time sampling. `npm run check` passed, including existing 721 planetary constraints. Real desktop and 390×844 browser checks are detailed in the scene LESSON.md. Controlled event-path test differs from OS hidden-tab observation. No real learner study or physical-phone testing. + +Preview/CI and production receipt will be filled with actual IDs after deployment; until then this is a reviewed candidate, not a released scene. diff --git a/docs/ai-learning/roadmap.json b/docs/ai-learning/roadmap.json index 88d49bb..79ab74d 100644 --- a/docs/ai-learning/roadmap.json +++ b/docs/ai-learning/roadmap.json @@ -33,9 +33,9 @@ "id": "embedding", "order": 2, "title": "编号怎样变成向量?", - "outcome": "跟踪查表得到的向量,并比较方向、距离与其教学边界。", - "status": "planned", - "route": null, + "outcome": "用编号定位词表的一行,取出向量;再用坐标比较距离与方向,区分编号与几何关系。", + "status": "reviewed", + "route": "/learn/embedding/", "published": false }, { diff --git a/evals/browser/embedding-lifecycle.html b/evals/browser/embedding-lifecycle.html new file mode 100644 index 0000000..3412c47 --- /dev/null +++ b/evals/browser/embedding-lifecycle.html @@ -0,0 +1,5 @@ +Embedding lifecycle QA

开发测试:向实际课堂文档分发受控 visibilitychange,验证其监听流程。此测试不等于真实操作系统切换标签页。

diff --git a/package.json b/package.json index 7d48663..bf2b3d2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "node scripts/build.mjs", - "test": "node evals/verify.mjs && node apps/site/learn/token-choice/model.test.mjs && node evals/verify-token-choice.mjs && node evals/verify-course.mjs && node apps/site/learn/tokenization/model.test.mjs", + "test": "node evals/verify.mjs && node apps/site/learn/token-choice/model.test.mjs && node evals/verify-token-choice.mjs && node evals/verify-course.mjs && node apps/site/learn/tokenization/model.test.mjs && node apps/site/learn/embedding/model.test.mjs", "check": "npm test && npm run build" }, "engines": { diff --git a/scripts/build.mjs b/scripts/build.mjs index 55ce3cd..9df719f 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -22,7 +22,7 @@ await write('start',page('让 Agent,多一个本领。','安装 Explain3D Skil await write('docs',page('怎么做,也怎么验证。','Explain3D 的结构、工作流、模板边界、自动发布与验证记录。',`

能力如何组成

Skill 负责把一个问题拆成对象、关系、变量、约束与叙事。运行时负责绝对时间采样、播放状态、视角、参数和配置退场。场景适配器负责各自的几何与计算。

产品采用可静态部署的网站,核心演示使用 Three.js;无 WebGL 时使用同一模型的 CPU 投影。官网不存储 API Key,也不直接运行用户提示词。

创建一个新场景

  1. 限定一个闭环问题与适合的受众。
  2. 写场景规格,注明来源、约束与简化。
  3. 选择合适的模板,重建新对象与计算关系。
  4. 共用时间、播放和交互状态,制作十秒讲解。
  5. 分别验证数学、交互、画面、键盘、手机与性能。
  6. 交付源码、提示词、运行方法与验证记录。

当前验证到哪里

已执行模型约束、时间线与静态构建检查,并在当前浏览器检查交互和响应式。当前云端验收浏览器禁用 WebGL,画面使用 CPU 兼容渲染。真实手机 GPU、方向传感器与持续帧率尚未实机验收。

标准 Skill 包已提供。Claude Code、WorkBuddy 和 DeepSeek 宿主的独立复现尚未完成;网站不会把文档适配标为验证通过。

自动发布

GitHub 保存网站、Skill、模板与提示词。Vercel Git 集成负责分支预览和正式分支发布;生产构建先执行 npm run check,模型检查失败时构建中止。每个版本可按 Git 提交追溯,正式站点绑定 explain.funagent.app。

Skill 的本地检查完成后再同步到发布目录。依赖、模型假设和第三方许可证保留在源码中。社交文案在素材页准备,发布到社交账号单独执行。

深度效果的边界

透视、遮挡、光照和运动视差能在普通屏幕中形成明显深度感;它们不让普通屏幕自动变成双眼立体硬件。倾斜控制依赖方向传感器,完整行星齿轮版可使用;其他模板支持拖动观察。

参与

提交新模板时,附带可运行场景、参数、公式或事件模型、简化说明和测试。请通过 GitHub Issues 提出问题或 Pull Request 分享作品。

仓库与贡献指南 ↗

`,'/docs/')); await write('press',page('把可以操作的理解,分享出去。','Explain3D 的真实演示、发布文案、品牌说明和素材。',`
Explain3D 实际交互作品预览

产品一句话

让 AI 把一个原理,做成可以亲手探索的 3D 作品。

可使用的宣传材料

小红书、短视频、公众号、X、社区与 GitHub 发布文案 ↗

下载分享封面 / 下载行星齿轮场景图

演示素材的准确表述

这是可操作的实时三维原理讲解。可以说“普通屏幕里的深度感”“可拖动、可拆解”“AI 辅助生成源码”。不能据此宣称普通手机拥有真正的双眼裸眼立体显示,也不能把未验证平台说成一键必成。

`,'/press/')); await buildCourse(root,out); -const urls=['/','/learn/','/learn/token-choice/','/learn/tokenization/','/templates/','/create/','/start/','/docs/','/press/',...templates.map(s=>`/templates/${s.id}/`)]; +const urls=['/','/learn/','/learn/token-choice/','/learn/tokenization/','/learn/embedding/','/templates/','/create/','/start/','/docs/','/press/',...templates.map(s=>`/templates/${s.id}/`)]; await fs.writeFile(path.join(out,'sitemap.xml'),`${urls.map(u=>`https://explain.funagent.app${u}`).join('')}`); await fs.writeFile(path.join(out,'robots.txt'),'User-agent: *\nAllow: /\nSitemap: https://explain.funagent.app/sitemap.xml\n'); await fs.writeFile(path.join(out,'favicon.svg'),'e');