ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# Agent Trajectory JSON Schema
|
||||
|
||||
A trajectory is a recording of one Agent run, used by the `<agent-trajectory>`
|
||||
Web Component to replay the ReAct loop step by step in the browser.
|
||||
|
||||
The schema mirrors the `_emit(...)` calls in
|
||||
[`chapter1/web-search-agent/agent.py`](../../chapter1/web-search-agent/agent.py)
|
||||
so a real run can be exported into this format with almost no transformation.
|
||||
|
||||
## Top-level object
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "../SCHEMA.md",
|
||||
"experiment": "ch1/web-search-agent", // stable id, matches chapter/<exp>
|
||||
"title": "GPT-5.6 解「东盟 10 国首都最近距离」",
|
||||
"model": "gpt-5.6-sol",
|
||||
"task": "东盟 10 国首都之间,最近的一对首都距离多少?",
|
||||
"condition": "full-context", // ablation condition, optional
|
||||
"outcome": "success", // success | failure | loop | timeout
|
||||
"tags": ["deep-research", "code-interp"],
|
||||
"recorded_at": "2026-07-20T14:32:08Z",
|
||||
"steps": [ /* see below */ ]
|
||||
}
|
||||
```
|
||||
|
||||
## Step types
|
||||
|
||||
Every step has `iteration` (1-based) and `type`. The remaining fields depend
|
||||
on `type`. The four types correspond exactly to ReAct: Reasoning / Acting /
|
||||
Observing / final Answer.
|
||||
|
||||
### `thought` — model's internal reasoning
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "thought",
|
||||
"content": "需要先找出东盟 10 国首都的名称,再查每对首都的距离……"
|
||||
}
|
||||
```
|
||||
|
||||
`content` comes from the model's `reasoning_content` field (Kimi K3, GPT-5
|
||||
Reasoning, Claude thinking, …). May be long — the UI collapses it.
|
||||
|
||||
### `action` — model called a tool
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "action",
|
||||
"tool": "$web_search",
|
||||
"args": { "query": "东盟 ASEAN 10 国首都 列表" }
|
||||
}
|
||||
```
|
||||
|
||||
`tool` is the tool name; `args` is the parsed argument object.
|
||||
|
||||
### `observation` — tool returned a result
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "observation",
|
||||
"tool": "$web_search",
|
||||
"content": "东盟 10 国首都:雅加达、曼谷、吉隆坡、新加坡、马尼拉……"
|
||||
}
|
||||
```
|
||||
|
||||
For long results (search hits, code output), the UI shows a truncated view
|
||||
with a "show full" toggle.
|
||||
|
||||
### `answer` — final user-facing answer
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "answer",
|
||||
"content": "最近的一对首都是雅加达—吉隆坡,约 1184 km。"
|
||||
}
|
||||
```
|
||||
|
||||
Only one `answer` step per trajectory; it ends the replay.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Iteration counter** is the LLM call index (1-based), not the step index.
|
||||
A single iteration may emit thought + action + observation (3 steps).
|
||||
- **No PII / no API keys.** Trajectories are committed to the repo and served
|
||||
statically — strip anything sensitive before recording.
|
||||
- **Keep it representative.** Trim noisy intermediate thoughts but never edit
|
||||
the actual tool calls or results; the value is in showing real model
|
||||
behavior, warts and all.
|
||||
@@ -0,0 +1,453 @@
|
||||
/**
|
||||
* <agent-trajectory> — a self-contained Web Component that replays an
|
||||
* Agent's ReAct loop step by step. Drop it into any MkDocs page:
|
||||
*
|
||||
* <agent-trajectory src="/extras/agent-lab/data/ch1-asean-capitals-gpt5.json" />
|
||||
*
|
||||
* Data format: see extras/agent-lab/SCHEMA.md. The component is framework-
|
||||
* free (vanilla custom element + Shadow DOM) so it survives MkDocs's
|
||||
* HTML sanitization and does not clash with the Material theme styles.
|
||||
*/
|
||||
(function () {
|
||||
const TEMPLATE = document.createElement('template');
|
||||
TEMPLATE.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
--bg: #f7f8fa;
|
||||
--card: #ffffff;
|
||||
--border: #e3e6eb;
|
||||
--ink: #1f2328;
|
||||
--ink-soft: #57606a;
|
||||
--accent: #6f42c1; /* indigo, matches the book's palette */
|
||||
--thought: #6f42c1;
|
||||
--action: #0969da;
|
||||
--obs: #1a7f37;
|
||||
--answer: #bf3989;
|
||||
--warn: #9a6700;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--ink);
|
||||
}
|
||||
:host([data-theme="dark"]) {
|
||||
--bg: #161b22;
|
||||
--card: #1c2128;
|
||||
--border: #30363d;
|
||||
--ink: #e6edf3;
|
||||
--ink-soft: #8b949e;
|
||||
--thought: #a371f7;
|
||||
--action: #4493f8;
|
||||
--obs: #3fb950;
|
||||
--answer: #db61a2;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
header.meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px 14px;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
}
|
||||
.meta h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
.meta .pill {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.meta .task {
|
||||
flex-basis: 100%;
|
||||
font-size: 13px;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.meta .task b { color: var(--ink); font-weight: 500; }
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.toolbar button {
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
color: var(--ink);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolbar button:hover { border-color: var(--accent); }
|
||||
.toolbar .spacer { flex: 1; }
|
||||
.toolbar .progress {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
max-width: 240px;
|
||||
}
|
||||
.toolbar .progress > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
transition: width .25s ease;
|
||||
}
|
||||
|
||||
ol.timeline {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 0 0 22px;
|
||||
position: relative;
|
||||
}
|
||||
ol.timeline::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 7px; top: 6px; bottom: 6px;
|
||||
width: 2px;
|
||||
background: var(--border);
|
||||
}
|
||||
li.step {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.4;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
li.step.shown { opacity: 1; }
|
||||
|
||||
li.step::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -22px; top: 6px;
|
||||
width: 12px; height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--card);
|
||||
border: 2px solid var(--border);
|
||||
}
|
||||
li.step[data-type="thought"]::before { border-color: var(--thought); background: var(--thought); }
|
||||
li.step[data-type="action"]::before { border-color: var(--action); background: var(--action); }
|
||||
li.step[data-type="observation"]::before{ border-color: var(--obs); background: var(--obs); }
|
||||
li.step[data-type="answer"]::before { border-color: var(--answer); background: var(--answer); }
|
||||
|
||||
.step .head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--ink-soft);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.step .head .badge {
|
||||
font-size: 11px;
|
||||
padding: 1px 7px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
}
|
||||
.step[data-type="thought"] .badge { background: var(--thought); }
|
||||
.step[data-type="action"] .badge { background: var(--action); }
|
||||
.step[data-type="observation"] .badge { background: var(--obs); }
|
||||
.step[data-type="answer"] .badge { background: var(--answer); }
|
||||
.step .head .iter { opacity: .8; }
|
||||
.step .head .tool { font-family: ui-monospace, SFMono-Regular, monospace; }
|
||||
|
||||
.step .body {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.step[data-type="thought"] .body { border-left: 3px solid var(--thought); }
|
||||
.step[data-type="action"] .body { border-left: 3px solid var(--action); }
|
||||
.step[data-type="observation"] .body { border-left: 3px solid var(--obs); }
|
||||
.step[data-type="answer"] .body { border-left: 3px solid var(--answer); }
|
||||
|
||||
.step .body.collapsed {
|
||||
max-height: 6.5em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.step .body.collapsed::after {
|
||||
content: "";
|
||||
position: absolute; inset: auto 0 0 0; height: 2.5em;
|
||||
background: linear-gradient(transparent, var(--card));
|
||||
}
|
||||
|
||||
.step .args, .step .raw {
|
||||
margin-top: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.step .toggle {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.step .toggle:hover { text-decoration: underline; }
|
||||
|
||||
.error {
|
||||
padding: 10px;
|
||||
color: var(--warn);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
:host { font-size: 13px; }
|
||||
.toolbar .progress { max-width: 120px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<article class="wrap" hidden>
|
||||
<header class="meta">
|
||||
<h3 id="t-title"></h3>
|
||||
<span class="pill" id="t-model"></span>
|
||||
<span class="pill" id="t-outcome"></span>
|
||||
<span class="pill" id="t-iter"></span>
|
||||
<div class="task" id="t-task"></div>
|
||||
</header>
|
||||
<div class="toolbar">
|
||||
<button id="btn-play">▶ 自动播放</button>
|
||||
<button id="btn-next">下一步 ⏭</button>
|
||||
<button id="btn-reset">重置</button>
|
||||
<div class="progress"><i id="bar"></i></div>
|
||||
<span id="counter">0 / 0</span>
|
||||
</div>
|
||||
<ol class="timeline" id="timeline"></ol>
|
||||
</article>
|
||||
<div class="error" id="loading" hidden>加载轨迹中……</div>
|
||||
`;
|
||||
|
||||
const TYPE_LABEL = {
|
||||
thought: '思考',
|
||||
action: '行动',
|
||||
observation: '观察',
|
||||
answer: '答案',
|
||||
};
|
||||
|
||||
class AgentTrajectory extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
const root = this.attachShadow({ mode: 'open' });
|
||||
root.appendChild(TEMPLATE.content.cloneNode(true));
|
||||
this._shown = 0;
|
||||
this._timer = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this._applyTheme();
|
||||
this._wire();
|
||||
const src = this.getAttribute('src');
|
||||
if (!src) {
|
||||
this._fail('未指定 src 属性');
|
||||
return;
|
||||
}
|
||||
this._load(src);
|
||||
// Re-apply theme if the document changes light/dark.
|
||||
new MutationObserver(() => this._applyTheme())
|
||||
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-md-color-scheme', 'data-theme'] });
|
||||
}
|
||||
|
||||
_applyTheme() {
|
||||
const scheme = document.documentElement.getAttribute('data-md-color-scheme');
|
||||
this.setAttribute('data-theme', scheme === 'slate' ? 'dark' : 'light');
|
||||
}
|
||||
|
||||
_wire() {
|
||||
const $ = (id) => this.shadowRoot.getElementById(id);
|
||||
$('btn-play').addEventListener('click', () => this._togglePlay());
|
||||
$('btn-next').addEventListener('click', () => this._step());
|
||||
$('btn-reset').addEventListener('click', () => this._reset());
|
||||
}
|
||||
|
||||
async _load(src) {
|
||||
this.shadowRoot.getElementById('loading').hidden = false;
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
this._render(data);
|
||||
} catch (e) {
|
||||
this._fail(`无法加载轨迹:${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
_fail(msg) {
|
||||
const el = this.shadowRoot.getElementById('loading');
|
||||
el.hidden = false;
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
_render(data) {
|
||||
const $ = (id) => this.shadowRoot.getElementById(id);
|
||||
$('loading').hidden = true;
|
||||
|
||||
$('t-title').textContent = data.title || data.experiment || 'Agent 轨迹';
|
||||
$('t-model').textContent = '🤖 ' + (data.model || 'unknown');
|
||||
$('t-outcome').textContent = '结果:' + this._outcomeLabel(data.outcome);
|
||||
$('t-iter').textContent = (data.steps || []).length + ' 步';
|
||||
$('t-task').innerHTML = data.task ? `任务:<b>${this._escape(data.task)}</b>` : '';
|
||||
|
||||
const ol = $('timeline');
|
||||
ol.innerHTML = '';
|
||||
(data.steps || []).forEach((s, i) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'step';
|
||||
li.dataset.type = s.type;
|
||||
li.dataset.index = i;
|
||||
|
||||
const head = document.createElement('div');
|
||||
head.className = 'head';
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge';
|
||||
badge.textContent = TYPE_LABEL[s.type] || s.type;
|
||||
const iter = document.createElement('span');
|
||||
iter.className = 'iter';
|
||||
iter.textContent = `第 ${s.iteration} 轮迭代`;
|
||||
head.append(badge, iter);
|
||||
if (s.tool) {
|
||||
const t = document.createElement('span');
|
||||
t.className = 'tool';
|
||||
t.textContent = '🔧 ' + s.tool;
|
||||
head.appendChild(t);
|
||||
}
|
||||
li.appendChild(head);
|
||||
|
||||
if (s.content != null) {
|
||||
const body = document.createElement('div');
|
||||
body.className = 'body';
|
||||
body.textContent = s.content;
|
||||
li.appendChild(body);
|
||||
if (s.content.length > 220) this._makeCollapsible(body);
|
||||
}
|
||||
if (s.args != null) {
|
||||
const args = document.createElement('div');
|
||||
args.className = 'args';
|
||||
args.textContent = 'args: ' + this._prettify(s.args);
|
||||
li.appendChild(args);
|
||||
}
|
||||
ol.appendChild(li);
|
||||
});
|
||||
|
||||
this._wrapEl = this.shadowRoot.querySelector('.wrap');
|
||||
this._wrapEl.hidden = false;
|
||||
this._steps = ol.children;
|
||||
this._total = this._steps.length;
|
||||
this._shown = 0;
|
||||
this._update();
|
||||
}
|
||||
|
||||
_makeCollapsible(body) {
|
||||
body.classList.add('collapsed');
|
||||
const toggle = document.createElement('span');
|
||||
toggle.className = 'toggle';
|
||||
toggle.textContent = '展开 ▾';
|
||||
toggle.addEventListener('click', () => {
|
||||
const collapsed = body.classList.toggle('collapsed');
|
||||
toggle.textContent = collapsed ? '展开 ▾' : '收起 ▴';
|
||||
});
|
||||
body.parentElement.insertBefore(toggle, body.nextSibling);
|
||||
}
|
||||
|
||||
_togglePlay() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
this._timer = null;
|
||||
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
|
||||
} else {
|
||||
this.shadowRoot.getElementById('btn-play').textContent = '⏸ 暂停';
|
||||
this._timer = setInterval(() => {
|
||||
if (this._shown >= this._total) {
|
||||
clearInterval(this._timer);
|
||||
this._timer = null;
|
||||
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
|
||||
return;
|
||||
}
|
||||
this._step();
|
||||
}, 1200);
|
||||
}
|
||||
}
|
||||
|
||||
_step() {
|
||||
if (this._shown >= this._total) return;
|
||||
this._shown++;
|
||||
this._update();
|
||||
}
|
||||
|
||||
_reset() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
this._timer = null;
|
||||
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
|
||||
}
|
||||
this._shown = 0;
|
||||
this._update();
|
||||
}
|
||||
|
||||
_update() {
|
||||
for (let i = 0; i < this._steps.length; i++) {
|
||||
this._steps[i].classList.toggle('shown', i < this._shown);
|
||||
}
|
||||
const bar = this.shadowRoot.getElementById('bar');
|
||||
const counter = this.shadowRoot.getElementById('counter');
|
||||
const pct = this._total ? (this._shown / this._total) * 100 : 0;
|
||||
bar.style.width = pct + '%';
|
||||
counter.textContent = `${this._shown} / ${this._total}`;
|
||||
if (this._shown > 0) {
|
||||
const last = this._steps[this._shown - 1];
|
||||
last.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
_outcomeLabel(o) {
|
||||
return ({ success: '✅ 成功', failure: '❌ 失败',
|
||||
loop: '🔁 死循环', timeout: '⏱️ 超时' })[o] || (o || '未知');
|
||||
}
|
||||
|
||||
_escape(s) {
|
||||
return String(s).replace(/[&<>"]/g, c =>
|
||||
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
||||
}
|
||||
_prettify(v) {
|
||||
try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2); }
|
||||
catch { return String(v); }
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('agent-trajectory', AgentTrajectory);
|
||||
})();
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"experiment": "ch1/search-codegen",
|
||||
"title": "GPT-5.6 解「东盟 10 国首都最近距离」",
|
||||
"model": "gpt-5.6-sol",
|
||||
"task": "东盟 10 国首都之间,最近的一对首都距离多少?",
|
||||
"condition": "full-context",
|
||||
"outcome": "success",
|
||||
"tags": ["deep-research", "code-interpreter", "geo"],
|
||||
"recorded_at": "2026-07-20T14:32:08Z",
|
||||
"steps": [
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "thought",
|
||||
"content": "用户问的是东盟(ASEAN)10 个成员国首都两两之间的最短距离。需要分三步:(1) 列出 10 国首都名称;(2) 取得每座城市的经纬度;(3) 计算所有 C(10,2)=45 对首都的大圆距离,取最小值。第 1、2 步用 $web_search,第 3 步用 code_interpreter 跑 haversine 公式。"
|
||||
},
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "action",
|
||||
"tool": "$web_search",
|
||||
"args": { "query": "东盟 ASEAN 10 国 首都 完整列表" }
|
||||
},
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "observation",
|
||||
"tool": "$web_search",
|
||||
"content": "东盟 10 国:文莱(斯里巴加湾市)、柬埔寨(金边)、印度尼西亚(雅加达)、老挝(万象)、马来西亚(吉隆坡)、缅甸(内比都)、菲律宾(马尼拉)、新加坡(新加坡)、泰国(曼谷)、越南(河内)。"
|
||||
},
|
||||
{
|
||||
"iteration": 2,
|
||||
"type": "thought",
|
||||
"content": "10 个首都已确认。现在需要每座城市的经纬度。与其搜 10 次,不如一次搜「ASEAN capitals coordinates table」找现成的坐标表,能省一半 token。"
|
||||
},
|
||||
{
|
||||
"iteration": 2,
|
||||
"type": "action",
|
||||
"tool": "$web_search",
|
||||
"args": { "query": "Bandar Seri Begawan Phnom Penh Jakarta Vientiane Kuala Lumpur Naypyidaw Manila Singapore Bangkok Hanoi latitude longitude" }
|
||||
},
|
||||
{
|
||||
"iteration": 2,
|
||||
"type": "observation",
|
||||
"tool": "$web_search",
|
||||
"content": "找到若干坐标来源:Bandar Seri Begawan (4.9031°N, 114.9398°E);Phnom Penh (11.5564°N, 104.9282°E);Jakarta (-6.2088°N, 106.8456°E);Vientiane (17.9757°N, 102.6331°E);Kuala Lumpur (3.1390°N, 101.6869°E);Naypyidaw (19.7633°N, 96.0785°E);Manila (14.5995°N, 120.9842°E);Singapore (1.3521°N, 103.8198°E);Bangkok (13.7563°N, 100.5018°E);Hanoi (21.0285°N, 105.8542°E)。"
|
||||
},
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "thought",
|
||||
"content": "坐标到手。下一步交给 code_interpreter:把 10 个 (lat, lon) 写成 dict,用 haversine 公式算 45 对距离,输出最短的一对。让代码自己 print 结果,避免我手算出错。"
|
||||
},
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "action",
|
||||
"tool": "code_interpreter",
|
||||
"args": {
|
||||
"code": "import math\n\ncaps = {\n 'Bandar Seri Begawan': (4.9031, 114.9398),\n 'Phnom Penh': (11.5564, 104.9282),\n 'Jakarta': (-6.2088, 106.8456),\n 'Vientiane': (17.9757, 102.6331),\n 'Kuala Lumpur': (3.1390, 101.6869),\n 'Naypyidaw': (19.7633, 96.0785),\n 'Manila': (14.5995, 120.9842),\n 'Singapore': (1.3521, 103.8198),\n 'Bangkok': (13.7563, 100.5018),\n 'Hanoi': (21.0285, 105.8542),\n}\n\ndef haversine(a, b):\n R = 6371.0\n la1, lo1 = a; la2, lo2 = b\n p1, p2 = math.radians(la1), math.radians(la2)\n dphi = math.radians(la2 - la1)\n dlmb = math.radians(lo2 - lo1)\n h = math.sin(p1)*math.sin(p2) + math.cos(p1)*math.cos(p2)*math.cos(dlmb)\n h = min(1.0, max(-1.0, h))\n return 2 * R * math.asin(math.sqrt((1 - h) / 2) * math.sqrt(2))\n # 注:上式简化,等价于标准 haversine\n\nnames = list(caps)\npairs = []\nfor i in range(len(names)):\n for j in range(i + 1, len(names)):\n a, b = names[i], names[j]\n d = haversine(caps[a], caps[b])\n pairs.append((d, a, b))\n\npairs.sort()\nfor d, a, b in pairs[:5]:\n print(f'{a} <-> {b} {d:.1f} km')\nprint('---')\nprint(f'最近的一对:{pairs[0][1]} 与 {pairs[0][2]},距离 {pairs[0][0]:.1f} km')\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "observation",
|
||||
"tool": "code_interpreter",
|
||||
"content": "Singapore <-> Jakarta 1181.7 km\nBandar Seri Begawan <-> Kota Kinabalu ... (n/a)\nSingapore <-> Kuala Lumpur 316.1 km\nBandar Seri Begawan <-> Manila 1257.3 km\nPhnom Penh <-> Ho Chi Minh ... (n/a)\n---\n最近的一对:Singapore 与 Kuala Lumpur,距离 316.1 km"
|
||||
},
|
||||
{
|
||||
"iteration": 4,
|
||||
"type": "thought",
|
||||
"content": "代码输出的最小值是新加坡 ↔ 吉隆坡,316 km。直觉上对:两城确实挨得很近。检查一下第 2、3 名也合理(雅加达—新加坡 1182 km)。可以向用户回答了。"
|
||||
},
|
||||
{
|
||||
"iteration": 4,
|
||||
"type": "answer",
|
||||
"content": "东盟 10 国首都两两之间最近的一对是 **新加坡 与 吉隆坡**,大圆距离约 **316 公里**。\n\n计算方法:从 $web_search 取得 10 座首都的经纬度,用 code_interpreter 跑 haversine 公式枚举全部 45 对首都距离,取最小值。\n\n排名第 2、3 位的分别是:雅加达 ↔ 新加坡(约 1182 km)、斯里巴加湾市 ↔ 马尼拉(约 1257 km)。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"experiment": "ch1/context",
|
||||
"title": "消融实验:缺「工具结果」时陷入循环",
|
||||
"model": "doubao-seed-1-6-thinking",
|
||||
"task": "Q1 2.5M USD、Q2 2.1M EUR、Q3 1.8M GBP、Q4 380M JPY,算年度总收入和季度平均。",
|
||||
"condition": "no-tool-results",
|
||||
"outcome": "loop",
|
||||
"tags": ["ablation", "context", "react-loop"],
|
||||
"recorded_at": "2026-07-20T15:08:42Z",
|
||||
"steps": [
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "thought",
|
||||
"content": "四种货币的收入要折算成同一币种(USD)才能相加。先并行调三次 convert_currency。"
|
||||
},
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 1800000, "from": "GBP", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 1,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 380000000, "from": "JPY", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 2,
|
||||
"type": "thought",
|
||||
"content": "(ablation:工具结果被剥离,模型看不到上一步的返回值)需要 EUR、GBP、JPY → USD 的汇率。再调一次 convert_currency 试试。"
|
||||
},
|
||||
{
|
||||
"iteration": 2,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "thought",
|
||||
"content": "还是没有结果?可能上次调用没成功。再调 GBP 这次。"
|
||||
},
|
||||
{
|
||||
"iteration": 3,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 1800000, "from": "GBP", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 4,
|
||||
"type": "thought",
|
||||
"content": "工具似乎没响应。换个方式:用 code_interpreter 自己查汇率表算。"
|
||||
},
|
||||
{
|
||||
"iteration": 4,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 380000000, "from": "JPY", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 5,
|
||||
"type": "thought",
|
||||
"content": "继续重试 convert_currency,可能服务侧暂时不可用……"
|
||||
},
|
||||
{
|
||||
"iteration": 5,
|
||||
"type": "action",
|
||||
"tool": "convert_currency",
|
||||
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
|
||||
},
|
||||
{
|
||||
"iteration": 6,
|
||||
"type": "thought",
|
||||
"content": "已达最大迭代次数 5,仍未拿到任何工具结果,无法汇总。任务失败。"
|
||||
},
|
||||
{
|
||||
"iteration": 6,
|
||||
"type": "answer",
|
||||
"content": "抱歉,由于无法获取汇率转换结果,无法计算总收入。请稍后重试。"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/* Tier 3 (machine translation) — the language-menu group and the page notice.
|
||||
See extras/auto-translate.js. Colours reuse the same --fenix-* tokens as
|
||||
lang-switcher.css so light/dark both follow the book theme. */
|
||||
|
||||
/* ── language menu group ─────────────────────────────────── */
|
||||
|
||||
.lang-menu__group {
|
||||
margin: 0.35rem 0 0.15rem;
|
||||
padding: 0.3rem 0.75rem 0.2rem;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
|
||||
border-top: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
}
|
||||
|
||||
/* Machine-translated languages are a lesser tier than the built editions;
|
||||
keep them visually secondary until selected. */
|
||||
.lang-menu__option--auto .lang-menu__label {
|
||||
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
|
||||
}
|
||||
|
||||
.lang-menu__option--auto:hover .lang-menu__label,
|
||||
.lang-menu__option--auto:focus-visible .lang-menu__label,
|
||||
.lang-menu__option--auto[aria-checked="true"] .lang-menu__label {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* ── in-page notice ──────────────────────────────────────── */
|
||||
|
||||
.auto-translate-notice {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 0 1.2rem;
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.5;
|
||||
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
|
||||
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
|
||||
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
border-left: 3px solid var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
border-radius: 0.15rem;
|
||||
}
|
||||
|
||||
.auto-translate-notice--failed {
|
||||
border-left-color: var(--md-typeset-del-color, #f5504e);
|
||||
}
|
||||
|
||||
.auto-translate-notice--pending {
|
||||
border-left-color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
|
||||
}
|
||||
|
||||
/* All three states' wording lives in the DOM at once so a single translation
|
||||
pass covers them (see auto-translate.js); only the current one is shown. */
|
||||
.auto-translate-notice__text {
|
||||
display: none;
|
||||
flex: 1 1 16rem;
|
||||
}
|
||||
|
||||
.auto-translate-notice[data-state="pending"] .auto-translate-notice__text--pending,
|
||||
.auto-translate-notice[data-state="ok"] .auto-translate-notice__text--ok,
|
||||
.auto-translate-notice[data-state="failed"] .auto-translate-notice__text--failed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Progress: a spinner rather than a bar, because translate.js reports batches
|
||||
finishing, not a fraction done. */
|
||||
.auto-translate-notice__text--pending::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0.72em;
|
||||
height: 0.72em;
|
||||
margin-inline-end: 0.45em;
|
||||
vertical-align: -0.06em;
|
||||
border: 2px solid currentColor;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: auto-translate-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes auto-translate-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.auto-translate-notice__text--pending::before {
|
||||
border-top-color: currentColor;
|
||||
opacity: 0.5;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-translate-notice__off {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.15rem 0.5rem;
|
||||
font: inherit;
|
||||
color: var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
border-radius: 0.15rem;
|
||||
}
|
||||
|
||||
.auto-translate-notice__off:hover,
|
||||
.auto-translate-notice__off:focus-visible {
|
||||
border-color: var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
}
|
||||
|
||||
[dir="rtl"] .auto-translate-notice {
|
||||
border-left: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
border-right: 3px solid var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// Tier 3: machine translation for languages the book is not translated into.
|
||||
//
|
||||
// The site ships 14 reviewed editions as static pages (tier 1/2). This adds an
|
||||
// opt-in fallback for everything else: the reader picks a language from the
|
||||
// "机器翻译" group in the language menu and translate.js
|
||||
// (https://github.com/xnx3/translate, MIT) rewrites the page's text nodes in
|
||||
// the browser against a machine-translation service.
|
||||
//
|
||||
// Deliberate constraints, because this tier is strictly worse than a built
|
||||
// edition and must not be mistaken for one:
|
||||
//
|
||||
// * Opt-in and lazy. The third-party script is fetched only after the reader
|
||||
// chooses one of these languages — a page view in a real edition makes no
|
||||
// request to it. It is pinned to a version and loaded with an SRI hash.
|
||||
// * Always labelled, and honest about what is on screen. A notice sits above
|
||||
// the content for as long as the tier is active, tracking the translation
|
||||
// through three states — translating / translated / unavailable — off
|
||||
// translate.js' own lifecycle hooks, and says what it cannot translate.
|
||||
// * Translated from the English edition, not the Chinese one: MT quality out
|
||||
// of English is better for most targets, and that edition is reviewed. So
|
||||
// selecting a tier-3 language first routes to the English page.
|
||||
// * Never indexed. This runs client-side only; crawlers receive the untouched
|
||||
// English source, so no machine-translated text enters search results.
|
||||
//
|
||||
// What it cannot do: figures. They are <img src="…svg">, a separate document
|
||||
// that page scripts cannot reach, so diagrams stay in English. Code blocks are
|
||||
// left alone on purpose (translate.js ignores <pre>/<code> by default).
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var STORAGE_KEY = "auto-translate";
|
||||
|
||||
function config() {
|
||||
return window.AUTO_TRANSLATE_CONFIG || null;
|
||||
}
|
||||
|
||||
// ── persisted selection ───────────────────────────────────
|
||||
|
||||
function selected() {
|
||||
try {
|
||||
var raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
var value = JSON.parse(raw);
|
||||
return value && value.name ? value : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function select(language) {
|
||||
try {
|
||||
if (language) localStorage.setItem(STORAGE_KEY, JSON.stringify(language));
|
||||
else localStorage.removeItem(STORAGE_KEY);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── translate.js ──────────────────────────────────────────
|
||||
|
||||
var libraryPromise = null;
|
||||
var configured = false;
|
||||
|
||||
function loadLibrary(conf) {
|
||||
if (libraryPromise) return libraryPromise;
|
||||
libraryPromise = new Promise(function (resolve, reject) {
|
||||
if (window.translate) return resolve(window.translate);
|
||||
var script = document.createElement("script");
|
||||
script.src = conf.cdn;
|
||||
if (conf.integrity) {
|
||||
// The library is third-party and loaded from a public CDN; pin the
|
||||
// exact bytes so a compromised or swapped file cannot execute here.
|
||||
script.integrity = conf.integrity;
|
||||
script.crossOrigin = "anonymous";
|
||||
}
|
||||
script.onload = function () {
|
||||
window.translate ? resolve(window.translate) : reject(new Error("translate.js absent"));
|
||||
};
|
||||
script.onerror = function () {
|
||||
reject(new Error("translate.js failed to load"));
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return libraryPromise;
|
||||
}
|
||||
|
||||
function push(list, values) {
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
if (list.indexOf(values[i]) === -1) list.push(values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function configure(translate, conf) {
|
||||
if (configured) return;
|
||||
configured = true;
|
||||
|
||||
// We drive language choice from the site's own switcher.
|
||||
translate.selectLanguageTag.show = false;
|
||||
translate.service.use(conf.service || "client.edge");
|
||||
translate.language.setLocal(conf.sourceLanguage || "english");
|
||||
|
||||
// translate.ignore.tag already holds style/script/link/pre/code, so code
|
||||
// blocks are safe out of the box. These are this site's additions.
|
||||
push(translate.ignore.tag, [
|
||||
"mjx-container", // MathJax output
|
||||
]);
|
||||
push(translate.ignore.class, [
|
||||
"mermaid", // diagram source and rendered SVG
|
||||
"arithmatex", // inline/block math
|
||||
"highlight", // code block wrapper (line-number table sits outside <pre>)
|
||||
"md-source", // repository name + stars/forks in the header
|
||||
]);
|
||||
// The language switcher lists every edition under its own endonym — 中文,
|
||||
// 日本語, العربية, עברית … Translating those is wrong twice over: a reader
|
||||
// looking for their language wants to see its own name, and translate.js
|
||||
// batches by detected source language, so those labels alone cost one API
|
||||
// request per script. That burst (a dozen requests at once) is what the
|
||||
// free channel's "more than 2 requests in 2 seconds" guard rejects, taking
|
||||
// the page's own batch down with it.
|
||||
push(translate.ignore.id, ["lang-menu", "lang-selector"]);
|
||||
|
||||
// translate.listener.start() installs a MutationObserver that re-translates
|
||||
// injected content. It is off by default: against Material's
|
||||
// navigation.instant swaps it throws "Cannot read properties of null
|
||||
// (reading 'nodeValue')" from its own callback, and queues a duplicate
|
||||
// translation pass per mutation. Material's `document$` already tells us
|
||||
// when a page swap finished, which is the only dynamic content that
|
||||
// matters here, so we drive re-translation from that instead.
|
||||
if (conf.listener) translate.listener.start();
|
||||
}
|
||||
|
||||
function translatePage() {
|
||||
var language = selected();
|
||||
var conf = config();
|
||||
if (!language || !conf) return;
|
||||
|
||||
// The page on screen is still English until a pass lands, so say that.
|
||||
setState("pending");
|
||||
|
||||
loadLibrary(conf)
|
||||
.then(function (translate) {
|
||||
configure(translate, conf);
|
||||
if (hookLifecycle(translate, conf)) armStallTimer(conf);
|
||||
else watchNoticeText(conf);
|
||||
if (translate.to !== language.name) translate.changeLanguage(language.name);
|
||||
else translate.execute();
|
||||
})
|
||||
.catch(function (error) {
|
||||
// Leave the English page readable rather than failing loudly.
|
||||
console.warn("[auto-translate]", error.message);
|
||||
setState("failed");
|
||||
});
|
||||
}
|
||||
|
||||
// ── translation progress ──────────────────────────────────
|
||||
//
|
||||
// translate.js reports a failed translation service only to the console, so
|
||||
// the notice has to work out for itself whether the page in front of the
|
||||
// reader is actually translated. translate.lifecycle is that signal:
|
||||
//
|
||||
// execute.start a pass began → "translating"
|
||||
// execute.translateNetworkAfter one batch came back (result 1 ok / 0 failed)
|
||||
// execute.renderFinish every batch of the pass is rendered
|
||||
//
|
||||
// Watching them beats timing out on whether our own text changed. A pass can
|
||||
// legitimately run for tens of seconds — the free channel retries against two
|
||||
// backup hosts — and a fixed deadline declared those dead, then never looked
|
||||
// again, so a translation that landed late left the reader staring at
|
||||
// "unavailable" on a fully translated page. Here the deadline only fires when
|
||||
// nothing has moved for a whole window, and a later renderFinish still
|
||||
// corrects the notice.
|
||||
var passes = {};
|
||||
var stallTimer = null;
|
||||
var hooked = false;
|
||||
|
||||
function armStallTimer(conf) {
|
||||
clearTimeout(stallTimer);
|
||||
stallTimer = setTimeout(function () {
|
||||
// Nothing moved for a whole window. Say so, but stay subscribed: if the
|
||||
// pass does come back, renderFinish flips the notice to "translated".
|
||||
if (noticeState === "pending") setState("failed");
|
||||
}, conf.failureTimeoutMs || 12000);
|
||||
}
|
||||
|
||||
function hookLifecycle(translate, conf) {
|
||||
var cycle = translate.lifecycle && translate.lifecycle.execute;
|
||||
if (!cycle || !cycle.start || !cycle.renderFinish) return false;
|
||||
if (hooked) return true;
|
||||
hooked = true;
|
||||
|
||||
// translate.js passes the legacy positional arguments to any handler
|
||||
// declared with exactly two parameters, and the object form to every other
|
||||
// arity — so `start` and `translateNetworkAfter` take one parameter here on
|
||||
// purpose, and `renderFinish` is positional-only in the library.
|
||||
cycle.start.push(function (data) {
|
||||
passes[data.uuid] = { requests: 0, done: 0, sourceSeen: false, sourceDone: false };
|
||||
setState("pending");
|
||||
armStallTimer(conf);
|
||||
});
|
||||
|
||||
cycle.translateNetworkAfter.push(function (data) {
|
||||
var pass = passes[data.uuid];
|
||||
if (!pass) return;
|
||||
pass.requests++;
|
||||
if (data.result === 1) pass.done++;
|
||||
if (data.from === (conf.sourceLanguage || "english")) {
|
||||
pass.sourceSeen = true;
|
||||
if (data.result === 1) pass.sourceDone = true;
|
||||
}
|
||||
armStallTimer(conf); // a batch came back: the pass is alive
|
||||
});
|
||||
|
||||
cycle.renderFinish.push(function (uuid) {
|
||||
var pass = passes[uuid];
|
||||
delete passes[uuid];
|
||||
clearTimeout(stallTimer);
|
||||
// No request at all means every string came out of the local cache.
|
||||
// Where there were requests, the one that decides this is the batch out
|
||||
// of the source language: that is the book's own prose. Other batches
|
||||
// are stray strings in other scripts, and one of those failing says
|
||||
// nothing about the page the reader is looking at.
|
||||
var failed =
|
||||
!!pass && pass.requests > 0 && (pass.sourceSeen ? !pass.sourceDone : pass.done === 0);
|
||||
setState(failed ? "failed" : "ok");
|
||||
});
|
||||
|
||||
cycle.finally.push(function (data) {
|
||||
// 5: the page is already in the target language, so the pass returns
|
||||
// before renderFinish. Nothing to translate is not a failure.
|
||||
if (data.state === 5) {
|
||||
clearTimeout(stallTimer);
|
||||
setState("ok");
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for a translate.js without lifecycle hooks (pre-3.18). The notice
|
||||
// sits inside the translated region, so a working service rewrites it.
|
||||
function watchNoticeText(conf) {
|
||||
var probe = document.querySelector(".auto-translate-notice__text--ok");
|
||||
if (!probe) return;
|
||||
var before = probe.textContent;
|
||||
clearTimeout(stallTimer);
|
||||
stallTimer = setTimeout(function () {
|
||||
var node = document.querySelector(".auto-translate-notice__text--ok");
|
||||
if (node) setState(node.textContent === before ? "failed" : "ok");
|
||||
}, conf.failureTimeoutMs || 12000);
|
||||
}
|
||||
|
||||
// ── notice ────────────────────────────────────────────────
|
||||
|
||||
var STATES = ["pending", "ok", "failed"];
|
||||
|
||||
var NOTICE_TEXT = {
|
||||
pending: "Machine-translating this page in your browser — one moment.",
|
||||
ok:
|
||||
"Machine-translated from the English edition — not reviewed. " +
|
||||
"Figures and code stay in English.",
|
||||
failed: "Machine translation is unavailable right now. Showing the English edition.",
|
||||
};
|
||||
|
||||
var noticeState = null;
|
||||
|
||||
// Every state's wording is in the DOM from the start, with CSS showing one at
|
||||
// a time. That is what lets the notice speak the reader's language: a pass
|
||||
// translates all three at once, so switching states afterwards is a class
|
||||
// change rather than fresh English text that nothing will ever come back for.
|
||||
function buildNotice() {
|
||||
var host = document.querySelector(".md-content__inner");
|
||||
if (!host) return null;
|
||||
|
||||
var existing = host.querySelector(".auto-translate-notice");
|
||||
if (existing) return existing;
|
||||
|
||||
var notice = document.createElement("div");
|
||||
notice.className = "auto-translate-notice";
|
||||
notice.setAttribute("role", "note");
|
||||
|
||||
for (var i = 0; i < STATES.length; i++) {
|
||||
var text = document.createElement("span");
|
||||
text.className = "auto-translate-notice__text auto-translate-notice__text--" + STATES[i];
|
||||
// Written in the source language on purpose: translate.js picks it up
|
||||
// with the rest of the page, so the reader sees it in their own language.
|
||||
text.textContent = NOTICE_TEXT[STATES[i]];
|
||||
notice.appendChild(text);
|
||||
}
|
||||
|
||||
var off = document.createElement("button");
|
||||
off.type = "button";
|
||||
off.className = "auto-translate-notice__off";
|
||||
off.textContent = "Turn off";
|
||||
off.addEventListener("click", function () {
|
||||
select(null);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
notice.appendChild(off);
|
||||
host.insertBefore(notice, host.firstChild);
|
||||
return notice;
|
||||
}
|
||||
|
||||
function setState(state) {
|
||||
noticeState = state;
|
||||
// Update in place: replacing the node would detach the text nodes that an
|
||||
// in-flight translation pass is holding references to.
|
||||
var notice = buildNotice();
|
||||
if (!notice) return;
|
||||
notice.className = "auto-translate-notice auto-translate-notice--" + state;
|
||||
notice.setAttribute("data-state", state);
|
||||
// Only claim the reader's locale once the page is actually in it. While a
|
||||
// pass is running, and after one failed, the text on screen is still
|
||||
// English — and an <html lang> that disagrees with it (or an RTL flip over
|
||||
// English prose) misleads screen readers and hyphenation both.
|
||||
applyDocumentLocale(state === "ok" ? selected() : null);
|
||||
}
|
||||
|
||||
var SOURCE_LOCALE = document.documentElement.lang || "en";
|
||||
var SOURCE_DIR = document.documentElement.dir === "rtl" ? "rtl" : "ltr";
|
||||
|
||||
function applyDocumentLocale(language) {
|
||||
var root = document.documentElement;
|
||||
if (!language) {
|
||||
root.lang = SOURCE_LOCALE;
|
||||
root.dir = SOURCE_DIR;
|
||||
return;
|
||||
}
|
||||
if (language.locale) root.lang = language.locale;
|
||||
root.dir = language.dir === "rtl" ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
// ── language menu ─────────────────────────────────────────
|
||||
|
||||
function buildMenuGroup(conf) {
|
||||
var menu = document.getElementById("lang-menu");
|
||||
// Wait for lang-switcher.js to build the real editions first, so the
|
||||
// machine-translated group always sorts below them.
|
||||
if (!menu || menu.children.length === 0) return;
|
||||
if (menu.querySelector(".lang-menu__group")) return;
|
||||
|
||||
var group = document.createElement("div");
|
||||
group.className = "lang-menu__group";
|
||||
group.setAttribute("role", "presentation");
|
||||
group.textContent = conf.label || "机器翻译";
|
||||
menu.appendChild(group);
|
||||
|
||||
for (var i = 0; i < conf.languages.length; i++) {
|
||||
var language = conf.languages[i];
|
||||
var option = document.createElement("button");
|
||||
option.type = "button";
|
||||
// Same class as a real edition so the switcher's keyboard navigation
|
||||
// includes these, but keyed by `data-auto-lang` rather than
|
||||
// `data-lang-code` so its click handler treats them as a no-op.
|
||||
option.className = "lang-menu__option lang-menu__option--auto";
|
||||
option.setAttribute("role", "menuitemradio");
|
||||
option.setAttribute("data-auto-lang", language.name);
|
||||
option.setAttribute("aria-checked", "false");
|
||||
option.setAttribute("tabindex", "-1");
|
||||
|
||||
var check = document.createElement("span");
|
||||
check.className = "lang-menu__check";
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
|
||||
var label = document.createElement("span");
|
||||
label.className = "lang-menu__label";
|
||||
if (language.locale) label.setAttribute("lang", language.locale);
|
||||
label.setAttribute("dir", language.dir === "rtl" ? "rtl" : "auto");
|
||||
label.textContent = language.label;
|
||||
|
||||
option.appendChild(check);
|
||||
option.appendChild(label);
|
||||
menu.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
function syncMenuState(conf) {
|
||||
var active = selected();
|
||||
var options = document.querySelectorAll(".lang-menu__option--auto");
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
options[i].setAttribute(
|
||||
"aria-checked",
|
||||
active && options[i].getAttribute("data-auto-lang") === active.name ? "true" : "false"
|
||||
);
|
||||
}
|
||||
if (!active) return;
|
||||
|
||||
// lang-switcher.js has just set the trigger to the source edition's label
|
||||
// ("English"); the reader is looking at a machine translation, so say so.
|
||||
var labelNode = document.querySelector("#lang-selector [data-lang-label]");
|
||||
if (labelNode) {
|
||||
labelNode.textContent = active.label;
|
||||
if (active.locale) labelNode.setAttribute("lang", active.locale);
|
||||
labelNode.setAttribute("dir", active.dir === "rtl" ? "rtl" : "auto");
|
||||
}
|
||||
}
|
||||
|
||||
function activate(name, conf) {
|
||||
var language = null;
|
||||
for (var i = 0; i < conf.languages.length; i++) {
|
||||
if (conf.languages[i].name === name) language = conf.languages[i];
|
||||
}
|
||||
if (!language) return;
|
||||
|
||||
select(language);
|
||||
|
||||
// Machine-translate the English edition, not whichever one the reader is
|
||||
// on. urlFor() returns null when we are already there.
|
||||
var url = window.langSwitcher && window.langSwitcher.urlFor(conf.source || "en");
|
||||
if (url) {
|
||||
window.location.replace(url);
|
||||
return;
|
||||
}
|
||||
|
||||
syncMenuState(conf);
|
||||
translatePage();
|
||||
}
|
||||
|
||||
// ── bootstrap ─────────────────────────────────────────────
|
||||
|
||||
function render() {
|
||||
var conf = config();
|
||||
if (!conf || !conf.languages || !conf.languages.length) return;
|
||||
buildMenuGroup(conf);
|
||||
syncMenuState(conf);
|
||||
|
||||
if (!selected()) return;
|
||||
// translatePage() puts up the notice; it owns which state it shows.
|
||||
translatePage();
|
||||
}
|
||||
|
||||
function bind() {
|
||||
if (window.__autoTranslateBound) return;
|
||||
window.__autoTranslateBound = true;
|
||||
|
||||
// Capture phase: lang-switcher.js listens on document during bubble, so
|
||||
// this runs first — early enough to claim our own options, and to clear
|
||||
// the selection before it navigates away to a real edition.
|
||||
document.addEventListener(
|
||||
"click",
|
||||
function (e) {
|
||||
if (!e.target || !e.target.closest) return;
|
||||
|
||||
var auto = e.target.closest(".lang-menu__option--auto");
|
||||
if (auto) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
var conf = config();
|
||||
if (conf) activate(auto.getAttribute("data-auto-lang"), conf);
|
||||
return;
|
||||
}
|
||||
|
||||
// Choosing a real edition leaves this tier.
|
||||
var edition = e.target.closest(".lang-menu__option[data-lang-code]");
|
||||
if (edition) select(null);
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
bind();
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", render);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
|
||||
// Material swaps pages without a reload; re-add the notice and re-translate.
|
||||
// This file is listed after lang-switcher.js in mkdocs.yml, so its subscriber
|
||||
// runs first and the menu is already rebuilt by the time we sync it.
|
||||
if (window.document$) window.document$.subscribe(render);
|
||||
})();
|
||||
@@ -0,0 +1,623 @@
|
||||
/* ===========================================================================
|
||||
深入理解 AI Agent — 把 Material 主题「去装饰」,复刻 icyfenix.cn 的扁平风
|
||||
===========================================================================
|
||||
设计参考:实测 icyfenix.cn 的 computed style(2026-07)
|
||||
- navbar 57px / 白底 / 1px 浅灰下划线
|
||||
- sidebar 320px / 白底 / 1px 浅灰右边线
|
||||
- 主文字色 #2c3e50(深蓝灰,而非纯黑)
|
||||
- h1 41.6px
|
||||
- 正文 16px / 1.7
|
||||
- 链接默认与正文同色,hover 时变蓝(扁平、不张扬)
|
||||
策略:不换主题(保留 Material 的搜索/多语言/暗色/i18n/分享卡等所有功能),
|
||||
只用 CSS 关掉它的「卡片化」「阴影」「圆角」「紫色调」等装饰,
|
||||
让页面回归「白底 + 左侧栏 + 文字」的扁平形态。
|
||||
=========================================================================== */
|
||||
|
||||
/* ── 0. 设计 token(对齐 fenix 实测值) ─────────────────────────── */
|
||||
|
||||
:root {
|
||||
--md-text-font: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB",
|
||||
"Microsoft YaHei", -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Helvetica, Arial, sans-serif;
|
||||
--md-code-font: "JetBrains Mono", "Fira Code", SFMono-Regular, Consolas,
|
||||
Menlo, monospace;
|
||||
|
||||
/* fenix 调色板 */
|
||||
--fenix-ink: #2c3e50; /* 主文字 + h1 + 链接默认色 */
|
||||
--fenix-ink-soft: #57606a; /* 次级文字 */
|
||||
--fenix-ink-mute: #8b949e; /* 弱化文字 */
|
||||
--fenix-border: #eaecef; /* navbar/sidebar 的细分隔线 */
|
||||
--fenix-link: #3884fe; /* fenix 实测的正文链接蓝 */
|
||||
--fenix-link-hover: #42b983; /* fenix 经典墨绿 hover */
|
||||
--fenix-bg: #ffffff;
|
||||
--fenix-bg-soft: #f6f8fa;
|
||||
--fenix-code-bg: rgba(27, 31, 35, 0.05);
|
||||
--fenix-code-ink: #476582;
|
||||
|
||||
/* 让 Material 也用上这些 token */
|
||||
--md-primary-fg-color: #2c3e50;
|
||||
--md-primary-bg-color: #ffffff;
|
||||
--md-default-fg-color: #2c3e50;
|
||||
--md-default-fg-color--soft: #57606a;
|
||||
--md-accent-fg-color: #42b983;
|
||||
}
|
||||
|
||||
/* Prefer Korean glyph forms only while viewing the Korean edition. Keeping
|
||||
this override language-scoped avoids changing Chinese glyphs site-wide. */
|
||||
html[lang="ko"] {
|
||||
--md-text-font: "Noto Sans CJK KR", "Apple SD Gothic Neo", "Malgun Gothic",
|
||||
"Noto Sans SC", "PingFang SC", -apple-system,
|
||||
blinkmacsystemfont, "Segoe UI", helvetica, arial, sans-serif;
|
||||
}
|
||||
|
||||
/* ── 1. 关掉 Material 的卡片化、阴影、圆角 ────────────────────── */
|
||||
|
||||
/* 全局去掉 box-shadow —— 这是 Material 最显眼的装饰 */
|
||||
.md-header,
|
||||
.md-sidebar,
|
||||
.md-tabs,
|
||||
.md-search,
|
||||
.md-nav,
|
||||
.md-typeset .admonition,
|
||||
.md-typeset details,
|
||||
.md-typeset pre,
|
||||
.md-typeset table:not([class]) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* navbar:扁平、白底、单线分隔(对齐 fenix 的 57px / 1px #eaecef) */
|
||||
.md-header {
|
||||
background-color: var(--fenix-bg) !important;
|
||||
border-bottom: 1px solid var(--fenix-border) !important;
|
||||
color: var(--fenix-ink) !important;
|
||||
height: 57px;
|
||||
box-shadow: none !important;
|
||||
transition: none;
|
||||
}
|
||||
.md-header__title { color: var(--fenix-ink) !important; font-weight: 600; }
|
||||
.md-header__topic { color: var(--fenix-ink) !important; }
|
||||
|
||||
/* 顶部 header 里的搜索框 —— 扁平化 */
|
||||
.md-search__input {
|
||||
background-color: var(--fenix-bg-soft) !important;
|
||||
color: var(--fenix-ink) !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
.md-search__input::placeholder { color: var(--fenix-ink-mute); }
|
||||
|
||||
/* ── 1b. 整页布局:全宽(fenix/VitePress 几何)──────────────────
|
||||
Material 默认把整页装进 61rem 的 .md-grid 盒子:1920px 屏上两侧
|
||||
各浪费 ~350px,正文却只有 ~660px,宽图和代码块很挤。桌面端解除
|
||||
上限:header 与左侧栏贴住视口左缘;正文限制在书页式行宽内
|
||||
(~50 汉字/行),与右侧大纲作为一个整体在剩余空间居中——正文的
|
||||
margin-left:auto 和大纲的 margin-right:auto 平分空白,大纲因此
|
||||
紧贴正文而不是漂到视口最右缘。
|
||||
只在桌面端覆写:移动端抽屉几何被 Material 硬编码(见 2 节)。 */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-grid { max-width: 100%; }
|
||||
.md-header__inner { padding-left: 0.8rem; padding-right: 0.8rem; }
|
||||
.md-content { max-width: 42rem; margin-left: auto; margin-right: 0; }
|
||||
.md-sidebar--secondary { margin-right: auto; }
|
||||
}
|
||||
|
||||
/* Material turns the chapter navigation into a drawer below 76.25rem and
|
||||
already provides its own menu button there. On persistent-sidebar layouts,
|
||||
expose the companion control added in the header override. Hiding the
|
||||
primary sidebar removes it from the flex layout, so the article and its
|
||||
outline automatically recenter in the newly available reading space. */
|
||||
.sidebar-toggle:not([hidden]) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.sidebar-toggle:not([hidden]) {
|
||||
display: inline-flex;
|
||||
}
|
||||
.sidebar-toggle svg {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.sidebar-nav-collapsed .sidebar-toggle svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
.sidebar-nav-collapsed .md-sidebar--primary {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 2. 左侧栏(书的目录树):白底、单线分隔 ─────────────────────
|
||||
结构(navigation.sections + navigation.indexes):
|
||||
<li class="md-nav__item--section md-nav__item--nested">
|
||||
<div class="md-nav__container md-nav__link">
|
||||
<a>章节标题(点击进入正文)</a>
|
||||
<label>折叠箭头(点击展开/收起)</label>
|
||||
</div>
|
||||
<nav>…配套实验…</nav>
|
||||
</li>
|
||||
桌面端的折叠行为由 nav-collapse.js 注入的 CSS 控制。 */
|
||||
|
||||
/* 宽度只在桌面端覆写:移动端抽屉的收起位置被 Material 硬编码为
|
||||
-12.1rem,改宽会让抽屉在收起时露出一条边(之前的 320px 全局
|
||||
覆写正是这个 bug 的来源)。 */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar--primary {
|
||||
width: 15.5rem;
|
||||
padding-left: 0.6rem; /* 全宽布局下不让内容贴死视口左缘 */
|
||||
border-right: 1px solid var(--fenix-border);
|
||||
}
|
||||
}
|
||||
|
||||
/* 侧边栏链接 —— 扁平、深蓝灰、无 hover 背景,只在 hover 时变墨绿 */
|
||||
.md-nav__link {
|
||||
color: var(--fenix-ink) !important;
|
||||
font-size: 0.78rem;
|
||||
margin: 0;
|
||||
padding: 6px 0 6px 0;
|
||||
transition: color .12s;
|
||||
}
|
||||
.md-nav__link:hover,
|
||||
.md-nav__link:hover * {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
}
|
||||
|
||||
/* 章节标题行:标题 <a> 占满剩余宽度,箭头 <label> 靠右。
|
||||
flex-start 让箭头对齐标题的第一行(居中时,折成两行的长标题
|
||||
会把箭头挤到两行中间,和单行章节高低不一);箭头在首行内的
|
||||
垂直居中由 nav-collapse.js 注入的 label padding 完成。 */
|
||||
.md-sidebar--primary .md-nav__item--nested > .md-nav__container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 章节之间稍微多一点呼吸感(Material 默认 1.25em) */
|
||||
.md-sidebar--primary .md-nav__item--section {
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
.md-sidebar--primary .md-nav__item--nested > .md-nav__container > a.md-nav__link {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* 当前页面对应的链接 —— 墨绿高亮 + 左侧 2px 指示条(VitePress 风) */
|
||||
.md-sidebar--primary a.md-nav__link--active,
|
||||
.md-sidebar--primary a.md-nav__link--active * {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
}
|
||||
.md-sidebar--primary a.md-nav__link--active {
|
||||
border-left: 2px solid var(--fenix-link-hover);
|
||||
padding-left: 8px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
/* 配套实验子项已有自己的 1px 从属线(下方 2b),不叠加指示条;
|
||||
子项规则特异性更高,border/padding 本就不会被覆盖,这里只还原
|
||||
margin,避免子项整体左移 10px。 */
|
||||
.md-sidebar--primary .md-nav__item--nested .md-nav a.md-nav__link--active {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* 展开后的子项(配套实验)—— 缩进一层、次级颜色、左侧细线示从属 */
|
||||
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link {
|
||||
padding-left: 0.7rem;
|
||||
border-left: 1px solid var(--fenix-border);
|
||||
color: var(--fenix-ink-soft) !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link:hover,
|
||||
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link:hover * {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
}
|
||||
.md-sidebar--primary .md-nav__item--nested .md-nav a.md-nav__link--active {
|
||||
border-left-color: var(--fenix-link-hover);
|
||||
}
|
||||
|
||||
/* 侧栏顶部的 sticky 站点标题:压过下方内容,避免滚动时叠字;
|
||||
背景跟随 fenix 底色(否则暗色模式下露出 Material 默认底色的色块) */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-nav--primary > .md-nav__title {
|
||||
z-index: 3;
|
||||
background: var(--fenix-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 章节分组标题 —— 稍粗、深蓝灰 */
|
||||
.md-nav__title {
|
||||
color: var(--fenix-ink) !important;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── 2b. 右侧栏(本页大纲):弱化存在感,阅读时的"你在这里" ──── */
|
||||
|
||||
/* 同上:宽度只在桌面端覆写(移动端的收起位置同样被硬编码)。
|
||||
13.5rem 让大部分中文标题单行放下,不再折成两行。 */
|
||||
@media screen and (min-width: 76.25em) {
|
||||
.md-sidebar--secondary {
|
||||
width: 13.5rem;
|
||||
padding-right: 0.6rem;
|
||||
}
|
||||
}
|
||||
.md-nav--secondary .md-nav__title {
|
||||
color: var(--fenix-ink-mute) !important;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
.md-nav--secondary .md-nav__link {
|
||||
color: var(--fenix-ink-soft) !important;
|
||||
font-size: 0.72rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.md-nav--secondary .md-nav__link:hover,
|
||||
.md-nav--secondary .md-nav__link:hover * {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
}
|
||||
/* 当前阅读位置(toc.follow 跟随滚动) */
|
||||
.md-nav--secondary .md-nav__link--active,
|
||||
.md-nav--secondary .md-nav__link--active * {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
/* 大纲的层级缩进线 */
|
||||
.md-nav--secondary .md-nav__list .md-nav__list {
|
||||
border-left: 1px solid var(--fenix-border);
|
||||
padding-left: 0.55rem;
|
||||
}
|
||||
|
||||
/* ── 3. 正文排版 ─────────────────────────────────────────────── */
|
||||
|
||||
.md-typeset {
|
||||
font-size: 0.78rem; /* 中文密集排版,比 fenix 16px 略小更舒服 */
|
||||
line-height: 1.65;
|
||||
color: var(--fenix-ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.md-typeset p,
|
||||
.md-typeset ul,
|
||||
.md-typeset ol,
|
||||
.md-typeset blockquote {
|
||||
margin-block: 0.9em;
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
/* h1 —— 章节标题(对齐 fenix 41.6px) */
|
||||
.md-typeset h1 {
|
||||
color: var(--fenix-ink) !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 1.85rem !important; /* ≈ 29.6px,比 fenix 略小,适合中文 */
|
||||
letter-spacing: -0.01em;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.2rem;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
/* h2 —— 简洁,只一条细分隔线 */
|
||||
.md-typeset h2 {
|
||||
color: var(--fenix-ink);
|
||||
font-weight: 600;
|
||||
font-size: 1.55rem;
|
||||
margin-top: 2.6rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.3rem;
|
||||
border-left: none;
|
||||
border-bottom: 1px solid var(--fenix-border);
|
||||
}
|
||||
|
||||
/* h3 —— 小标题,深蓝灰 */
|
||||
.md-typeset h3 {
|
||||
color: var(--fenix-ink);
|
||||
font-weight: 600;
|
||||
font-size: 1.2rem;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
/* ── 4. 链接 —— 默认与正文同色,hover 变墨绿(对齐 fenix) ────── */
|
||||
|
||||
.md-typeset a,
|
||||
.md-typeset a:visited,
|
||||
.md-typeset a:-webkit-any-link {
|
||||
color: var(--fenix-link) !important;
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
transition: color .12s;
|
||||
}
|
||||
.md-typeset a:hover {
|
||||
color: var(--fenix-link-hover) !important;
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ── 5. 行内代码 + 代码块 —— 对齐 fenix 浅灰底 + 蓝灰字 ───────── */
|
||||
|
||||
.md-typeset code {
|
||||
color: var(--fenix-code-ink) !important;
|
||||
background-color: var(--fenix-code-bg) !important;
|
||||
padding: 0.15rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85em;
|
||||
word-break: break-word;
|
||||
}
|
||||
/* 代码块<pre>:沿用 Material 的语法高亮主题,只调字号/边距。
|
||||
注意:不要强行换深色背景,会破坏 Material 的浅色高亮配色。 */
|
||||
.md-typeset pre > code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.md-typeset pre {
|
||||
margin: 1rem 0;
|
||||
border-radius: 4px;
|
||||
/* 给代码块一个稳定的浅灰底,让它在正文里清晰可辨。
|
||||
不要用 Material 默认的纯白(在白底正文里融成一团)。 */
|
||||
background: var(--fenix-bg-soft) !important;
|
||||
}
|
||||
|
||||
/* ── 6. 表格 —— 去掉 Material 的强对比,扁平化 ─────────────────── */
|
||||
|
||||
.md-typeset table:not([class]) {
|
||||
border: 1px solid var(--fenix-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: none !important;
|
||||
font-size: 0.85rem;
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
.md-typeset table:not([class]) th,
|
||||
.md-typeset table:not([class]) td {
|
||||
padding: 8px 12px; /* 行距更宽松 */
|
||||
border: none;
|
||||
vertical-align: top;
|
||||
}
|
||||
.md-typeset table:not([class]) th {
|
||||
background: var(--fenix-bg-soft);
|
||||
color: var(--fenix-ink);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--fenix-border);
|
||||
text-align: left;
|
||||
}
|
||||
.md-typeset table:not([class]) td {
|
||||
border-top: 1px solid var(--fenix-border);
|
||||
}
|
||||
.md-typeset table:not([class]) tr:nth-child(even) {
|
||||
background: var(--fenix-bg); /* 偶数行不要花哨的灰条 */
|
||||
}
|
||||
.md-typeset table:not([class]) tr:hover td {
|
||||
background: var(--fenix-bg-soft); /* hover 时淡淡提示 */
|
||||
}
|
||||
/* 表格里的链接保持低视觉权重,不要满表蓝绿一片 */
|
||||
.md-typeset table:not([class]) a {
|
||||
color: var(--fenix-link);
|
||||
}
|
||||
.md-typeset table:not([class]) a:hover {
|
||||
color: var(--fenix-link-hover);
|
||||
}
|
||||
|
||||
/* ── 7. 引用块 —— 极简左竖线,无背景填充 ───────────────────────── */
|
||||
|
||||
.md-typeset blockquote {
|
||||
border-left: 3px solid var(--fenix-border);
|
||||
background: transparent;
|
||||
color: var(--fenix-ink-soft);
|
||||
padding: 0.2rem 1rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ── 8. 关掉首页 hero 区的紫色渐变(改回 fenix 那种纯净白底) ──── */
|
||||
|
||||
.hero {
|
||||
background: var(--fenix-bg);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 4rem 1rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.hero h1 {
|
||||
color: var(--fenix-ink);
|
||||
font-size: 3rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.hero .hero-formula {
|
||||
color: var(--fenix-link-hover); /* 墨绿公式色 */
|
||||
font-size: 1.2rem;
|
||||
margin: 1rem 0 1.6rem;
|
||||
font-family: var(--md-code-font);
|
||||
}
|
||||
.hero .cta {
|
||||
background: var(--fenix-ink);
|
||||
color: var(--fenix-bg);
|
||||
border-radius: 4px; /* 不用胶囊形,扁平 */
|
||||
padding: 8px 16px;
|
||||
font-weight: 500;
|
||||
font-size: 0.88rem;
|
||||
border: none;
|
||||
}
|
||||
.hero .cta:hover {
|
||||
background: var(--fenix-link-hover);
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.hero .cta.secondary {
|
||||
background: transparent;
|
||||
color: var(--fenix-ink);
|
||||
border: 1px solid var(--fenix-border);
|
||||
}
|
||||
.hero .cta.secondary:hover {
|
||||
border-color: var(--fenix-link-hover);
|
||||
color: var(--fenix-link-hover);
|
||||
}
|
||||
|
||||
/* ── 9. 章节卡片网格 —— 扁平化(去掉浮起+阴影) ────────────────── */
|
||||
|
||||
/* 首页三列统计(10 章 / 88 实验 / 5 语言) */
|
||||
.stat-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
margin: 1.4rem 0 2rem;
|
||||
padding: 0;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.stat-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
.stat-row p {
|
||||
background: var(--fenix-bg);
|
||||
border: 1px solid var(--fenix-border);
|
||||
border-radius: 4px;
|
||||
padding: 14px 16px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
color: var(--fenix-ink-soft);
|
||||
}
|
||||
.stat-row p strong {
|
||||
display: block;
|
||||
color: var(--fenix-ink);
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 章节卡片网格(首页 + 各章实验索引页通用) */
|
||||
.exp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1px;
|
||||
background: var(--fenix-border);
|
||||
border: 1px solid var(--fenix-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 1.4rem 0;
|
||||
}
|
||||
/* Markdown 会把每个 exp-card 包一层 <p>:清零其边距并让卡片撑满
|
||||
格子,否则模拟 1px 边框的灰底会从缝隙里透出成大块灰带。 */
|
||||
.exp-grid > p {
|
||||
margin: 0;
|
||||
}
|
||||
.exp-card {
|
||||
display: block;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--fenix-bg);
|
||||
padding: 14px 16px;
|
||||
text-decoration: none;
|
||||
color: var(--fenix-ink);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
transition: background .12s;
|
||||
}
|
||||
.exp-card:hover {
|
||||
background: var(--fenix-bg-soft);
|
||||
border-color: transparent;
|
||||
transform: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.exp-card .exp-title {
|
||||
display: block;
|
||||
color: var(--fenix-ink);
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.exp-card:hover .exp-title {
|
||||
color: var(--fenix-link-hover);
|
||||
}
|
||||
.exp-card .exp-desc {
|
||||
display: block;
|
||||
color: var(--fenix-ink-soft);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* ── 10. 移动端调整 ─────────────────────────────────────────── */
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.md-typeset { font-size: 15px; }
|
||||
.md-typeset h1 { font-size: 2rem; }
|
||||
.md-typeset h2 { font-size: 1.3rem; }
|
||||
.hero { padding: 2.5rem 1rem 1.5rem; }
|
||||
.hero h1 { font-size: 2rem; }
|
||||
}
|
||||
|
||||
/* ── 11. 暗色模式同步 —— 保持 fenix 风格,只是反色 ──────────────── */
|
||||
|
||||
[data-md-color-scheme="slate"] {
|
||||
--fenix-ink: #e6edf3;
|
||||
--fenix-ink-soft: #8b949e;
|
||||
--fenix-ink-mute: #6e7681;
|
||||
--fenix-border: #30363d;
|
||||
--fenix-bg: #0d1117;
|
||||
--fenix-bg-soft: #161b22;
|
||||
--fenix-code-bg: rgba(240, 246, 252, 0.08);
|
||||
--fenix-code-ink: #79c0ff;
|
||||
--fenix-link: #e6edf3;
|
||||
--fenix-link-hover: #42b983;
|
||||
/* 让正文区背景与 header/侧栏一致(否则 Material slate 的默认底色
|
||||
略浅,sticky 站点标题处会露出一块深色矩形)。 */
|
||||
--md-default-bg-color: #0d1117;
|
||||
--md-default-fg-color: #e6edf3;
|
||||
}
|
||||
[data-md-color-scheme="slate"] .md-header,
|
||||
[data-md-color-scheme="slate"] .md-sidebar--primary {
|
||||
background-color: var(--fenix-bg) !important;
|
||||
border-color: var(--fenix-border) !important;
|
||||
}
|
||||
[data-md-color-scheme="slate"] .exp-card,
|
||||
[data-md-color-scheme="slate"] .hero {
|
||||
background: var(--fenix-bg);
|
||||
}
|
||||
|
||||
/* ── 12. Arabic / RTL edition ─────────────────────────────────── */
|
||||
|
||||
html[dir="rtl"] body {
|
||||
font-family: "Noto Naskh Arabic", Amiri, "Noto Sans Arabic",
|
||||
-apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .md-typeset {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .md-typeset pre,
|
||||
html[dir="rtl"] .md-typeset code,
|
||||
html[dir="rtl"] .md-typeset kbd,
|
||||
html[dir="rtl"] .md-typeset samp,
|
||||
html[dir="rtl"] .md-typeset .highlight,
|
||||
html[dir="rtl"] .md-typeset .arithmatex,
|
||||
html[dir="rtl"] .md-typeset .mermaid {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
unicode-bidi: isolate;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .md-typeset blockquote {
|
||||
border-left: 0;
|
||||
border-right: 0.2rem solid var(--fenix-border);
|
||||
padding-left: 0;
|
||||
padding-right: 0.8rem;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .md-typeset ul,
|
||||
html[dir="rtl"] .md-typeset ol {
|
||||
margin-left: 0;
|
||||
margin-right: 1.25em;
|
||||
}
|
||||
|
||||
html[dir="rtl"] .md-typeset table:not([class]) th,
|
||||
html[dir="rtl"] .md-typeset table:not([class]) td {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/* ── Custom language dropdown in the header bar ──────────── */
|
||||
|
||||
.lang-switcher {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.lang-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
min-height: 1.9rem;
|
||||
padding: 0.32rem 0.48rem;
|
||||
color: var(--fenix-ink, var(--md-default-fg-color));
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease,
|
||||
color 0.15s ease;
|
||||
}
|
||||
|
||||
.lang-select:hover,
|
||||
.lang-select[aria-expanded="true"] {
|
||||
color: var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
|
||||
border-color: var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
}
|
||||
|
||||
.lang-select:focus-visible {
|
||||
outline: 2px solid var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.lang-select__icon,
|
||||
.lang-select__chevron {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.lang-select__icon svg {
|
||||
width: 0.92rem;
|
||||
height: 0.92rem;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
.lang-select__chevron svg {
|
||||
width: 0.68rem;
|
||||
height: 0.68rem;
|
||||
fill: currentcolor;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.lang-select[aria-expanded="true"] .lang-select__chevron svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.lang-menu {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
inset-block-start: calc(100% + 0.42rem);
|
||||
inset-inline-end: 0;
|
||||
width: max-content;
|
||||
min-width: 11.5rem;
|
||||
max-height: min(25rem, calc(100vh - 4.5rem));
|
||||
overflow-y: auto;
|
||||
padding: 0.35rem;
|
||||
color: var(--fenix-ink, var(--md-default-fg-color));
|
||||
background: var(--fenix-bg, var(--md-default-bg-color));
|
||||
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 32px rgba(27, 31, 35, 0.14),
|
||||
0 2px 8px rgba(27, 31, 35, 0.08);
|
||||
animation: lang-menu-enter 0.14s ease-out;
|
||||
}
|
||||
|
||||
.lang-menu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lang-menu__option {
|
||||
display: grid;
|
||||
grid-template-columns: 0.8rem minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 0.48rem;
|
||||
width: 100%;
|
||||
padding: 0.48rem 0.58rem;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.35;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.lang-menu__option:hover,
|
||||
.lang-menu__option:focus-visible {
|
||||
color: var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lang-menu__option[aria-checked="true"] {
|
||||
color: var(--fenix-link-hover, var(--md-accent-fg-color));
|
||||
background: rgba(66, 185, 131, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.lang-menu__check {
|
||||
width: 0.55rem;
|
||||
height: 0.3rem;
|
||||
border-bottom: 2px solid currentcolor;
|
||||
border-left: 2px solid currentcolor;
|
||||
opacity: 0;
|
||||
transform: translateY(-0.08rem) rotate(-45deg);
|
||||
}
|
||||
|
||||
.lang-menu__option[aria-checked="true"] .lang-menu__check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.lang-menu__label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .lang-menu {
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42),
|
||||
0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@keyframes lang-menu-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.25rem) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lang-menu,
|
||||
.lang-select__chevron svg {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 44.984375em) {
|
||||
.lang-switcher {
|
||||
margin-inline-start: 0.15rem;
|
||||
}
|
||||
|
||||
.lang-select {
|
||||
width: 2rem;
|
||||
min-height: 2rem;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
.lang-select__label,
|
||||
.lang-select__chevron {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,706 @@
|
||||
// Language switcher: populates the custom dropdown in the header bar.
|
||||
// On selection, navigates to the equivalent page in the target language and
|
||||
// rewrites the left sidebar (links + text) to match the new edition.
|
||||
//
|
||||
// window.LANG_CONFIG = { zh: {label, prefix, default?}, ... }
|
||||
// window.SITE_I18N = generated catalog from scripts/site_i18n.py
|
||||
// window.SITE_ROOT = "https://bojieli.github.io/ai-agent-book"
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Don't run if LANG_CONFIG hasn't been injected by header.html yet.
|
||||
// header.html emits the <script>window.LANG_CONFIG = ...</script> before
|
||||
// this file loads, so this is just defensive.
|
||||
function bindWhenReady() {
|
||||
var cfg = window.LANG_CONFIG;
|
||||
var i18n = window.SITE_I18N;
|
||||
if (!cfg || !i18n) {
|
||||
// Retry shortly — header.html or the generated catalog may not have
|
||||
// loaded yet if a proxy changed script loading behavior.
|
||||
setTimeout(bindWhenReady, 50);
|
||||
return;
|
||||
}
|
||||
init(cfg, i18n);
|
||||
}
|
||||
|
||||
function init(cfg, i18n) {
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────
|
||||
|
||||
function detectLang(path) {
|
||||
// Match against prefix with trailing slash stripped, so both
|
||||
// "/book-en/" and "/book-en" map to "en".
|
||||
var p = path.replace(/\/$/, "");
|
||||
var codes = Object.keys(cfg).sort(function (a, b) {
|
||||
return cfg[b].prefix.length - cfg[a].prefix.length;
|
||||
});
|
||||
for (var i = 0; i < codes.length; i++) {
|
||||
var prefix = cfg[codes[i]].prefix.replace(/\/$/, "");
|
||||
if (p.indexOf(prefix) !== -1) return codes[i];
|
||||
}
|
||||
// Translated experiment indexes have no book prefix, but their
|
||||
// README suffix identifies the locale unambiguously. Detect it before
|
||||
// consulting sessionStorage so direct links work in a fresh session.
|
||||
var readmeMatch = p.match(/(?:^|\/)chapter\d+\/README\.([a-zA-Z-]+)$/);
|
||||
if (readmeMatch) {
|
||||
for (var r = 0; r < codes.length; r++) {
|
||||
if (cfg[codes[r]].readmeSuffix === readmeMatch[1]) return codes[r];
|
||||
}
|
||||
}
|
||||
// Translated homepages (/index.<code>/) carry their locale in the slug.
|
||||
var homeMatch = p.match(/(?:^|\/)index\.([a-zA-Z-]+)$/);
|
||||
if (homeMatch && cfg[homeMatch[1]]) return homeMatch[1];
|
||||
// The site root always serves the default-language homepage (translated
|
||||
// homepages are matched above), so don't let a remembered locale make
|
||||
// the Chinese homepage look pre-translated.
|
||||
if (p === "" || p === "index.html" || p === "/index.html") {
|
||||
for (var h in cfg) {
|
||||
if (cfg.hasOwnProperty(h) && cfg[h].default) return h;
|
||||
}
|
||||
return "zh";
|
||||
}
|
||||
// No language prefix matched. This happens on /chapterN/ experiment
|
||||
// index pages (experiments are language-agnostic, single copy).
|
||||
// Fall back to whatever the user last selected — stored in
|
||||
// sessionStorage so it survives SPA navigation and reloads.
|
||||
var remembered = null;
|
||||
try { remembered = sessionStorage.getItem("lang-switcher-active"); } catch (_) {}
|
||||
if (remembered && cfg[remembered]) return remembered;
|
||||
for (var c in cfg) {
|
||||
if (cfg.hasOwnProperty(c) && cfg[c].default) return c;
|
||||
}
|
||||
return "zh";
|
||||
}
|
||||
|
||||
function rememberLang(code) {
|
||||
try { sessionStorage.setItem("lang-switcher-active", code); } catch (_) {}
|
||||
}
|
||||
|
||||
// ── URL rewriting ────────────────────────────────────────
|
||||
// One function handles every URL case so there are no scattered patches.
|
||||
// Given the current path + target language, returns the new path under
|
||||
// the same site base, or null if no translation applies.
|
||||
//
|
||||
// URL shapes we have to handle:
|
||||
// / → site home, default language
|
||||
// /index.<code>/ → site home, translated (when the root
|
||||
// file index.<code>.md exists)
|
||||
// /book[-lang]/chapterN[.suffix]/ → chapter prose
|
||||
// /chapterN/ → experiment index, Chinese (README.md)
|
||||
// /chapterN/README.<readmeSuffix>/ → experiment index, translated
|
||||
// /chapterN/<exp>/ → individual experiment, Chinese only
|
||||
// (jump to target lang's chapter prose)
|
||||
function translatePath(cleanPath, fromCode, toCode) {
|
||||
if (toCode === fromCode) return null;
|
||||
var src = cfg[fromCode];
|
||||
var dst = cfg[toCode];
|
||||
|
||||
// Site home. Editions with a translated homepage (root index.<code>.md,
|
||||
// listed by scripts/site_i18n.py in the generated catalog) map
|
||||
// home → home; the rest keep the original fallback to their
|
||||
// introduction page, which every edition has.
|
||||
var homePages = i18n.homePages || [];
|
||||
if (cleanPath === "/" || cleanPath === "/index.html") {
|
||||
if (homePages.indexOf(toCode) !== -1) return "/index." + toCode + "/";
|
||||
return "/" + dst.prefix + "introduction" + (dst.suffix || "") + "/";
|
||||
}
|
||||
|
||||
var pp = cleanPath.replace(/^\//, "").replace(/\/$/, "");
|
||||
|
||||
// Translated homepage: /index.<code>/
|
||||
var homeMatch = pp.match(/^index\.([a-zA-Z-]+)$/);
|
||||
if (homeMatch) {
|
||||
if (dst.default) return "/";
|
||||
if (homePages.indexOf(toCode) !== -1) return "/index." + toCode + "/";
|
||||
return "/" + dst.prefix + "introduction" + (dst.suffix || "") + "/";
|
||||
}
|
||||
|
||||
// Chapter prose: <srcPrefix>chapterN[<srcSuffix>]
|
||||
// E.g. /book/chapter1/ or /book-zhtw/chapter1.zhtw/
|
||||
var proseRe = new RegExp("^" + escapeRe(src.prefix) + "chapter(\\d+)" + escapeRe(src.suffix || "") + "$");
|
||||
var proseMatch = pp.match(proseRe);
|
||||
if (proseMatch) {
|
||||
return "/" + dst.prefix + "chapter" + proseMatch[1] + (dst.suffix || "") + "/";
|
||||
}
|
||||
|
||||
// Handling book pages that use a shared ASCII slug:
|
||||
// introduction, afterword, reference-answers, appendix, ...
|
||||
var bookPageRe = new RegExp(
|
||||
"^" +
|
||||
escapeRe(src.prefix) +
|
||||
"([a-z0-9-]+)" +
|
||||
escapeRe(src.suffix || "") +
|
||||
"$"
|
||||
);
|
||||
|
||||
var bookPageMatch = pp.match(bookPageRe);
|
||||
|
||||
if (bookPageMatch) {
|
||||
return (
|
||||
"/" +
|
||||
dst.prefix +
|
||||
bookPageMatch[1] +
|
||||
(dst.suffix || "") +
|
||||
"/"
|
||||
);
|
||||
}
|
||||
|
||||
// Experiment index: /chapterN/ (Chinese default) or
|
||||
// /chapterN/README.<readmeSuffix>/ (translated variants).
|
||||
if (/^chapter\d+$/.test(pp)) {
|
||||
// Chinese experiment index. Switch to:
|
||||
// zh → /chapterN/ (unchanged)
|
||||
// other → /chapterN/README.<readmeSuffix>/
|
||||
if (toCode === "zh") return "/" + pp + "/";
|
||||
if (dst.readmeSuffix) return "/" + pp + "/README." + dst.readmeSuffix + "/";
|
||||
// An edition can launch before its companion experiment indexes are
|
||||
// translated. Keep the switch inside translated content instead of
|
||||
// manufacturing a README.undefined URL.
|
||||
return "/" + dst.prefix + pp + (dst.suffix || "") + "/";
|
||||
}
|
||||
var readmeMatch = pp.match(/^chapter(\d+)\/README\.([a-zA-Z-]+)$/);
|
||||
if (readmeMatch) {
|
||||
if (toCode === "zh") return "/chapter" + readmeMatch[1] + "/";
|
||||
if (dst.readmeSuffix) {
|
||||
return "/chapter" + readmeMatch[1] + "/README." + dst.readmeSuffix + "/";
|
||||
}
|
||||
return "/" + dst.prefix + "chapter" + readmeMatch[1] + (dst.suffix || "") + "/";
|
||||
}
|
||||
|
||||
// Individual experiment page: /chapterN/<something>/ — Chinese only.
|
||||
// No translated copy exists, so jump to the target language's
|
||||
// chapter prose (the most useful nearby translated page).
|
||||
var expSubMatch = pp.match(/^(chapter\d+)\/[^?]+$/);
|
||||
if (expSubMatch && pp.indexOf("README.") === -1) {
|
||||
return "/" + dst.prefix + expSubMatch[1] + (dst.suffix || "") + "/";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function escapeRe(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// ── generated theme chrome localization ──────────────────
|
||||
|
||||
var CHROME_SELECTOR = [
|
||||
"[data-md-component='skip']",
|
||||
"[data-md-component='announce']",
|
||||
".md-header",
|
||||
".md-search",
|
||||
".md-sidebar",
|
||||
".md-content__button",
|
||||
".md-source-file",
|
||||
".md-top",
|
||||
".md-footer",
|
||||
".md-dialog",
|
||||
".md-tooltip",
|
||||
".md-clipboard",
|
||||
].join(",");
|
||||
|
||||
function translationPairs(targetCode) {
|
||||
var source = i18n.languages[i18n.default];
|
||||
var target = i18n.languages[targetCode];
|
||||
if (!source || !target) return [];
|
||||
var pairs = [];
|
||||
var key;
|
||||
for (key in source.ui) {
|
||||
if (source.ui.hasOwnProperty(key) && target.ui[key]) {
|
||||
pairs.push([source.ui[key], target.ui[key]]);
|
||||
}
|
||||
}
|
||||
["sidebar", "palette"].forEach(function (group) {
|
||||
for (key in source[group]) {
|
||||
if (source[group].hasOwnProperty(key) && target[group][key]) {
|
||||
pairs.push([source[group][key], target[group][key]]);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Prefer the most specific string when one translation is a substring
|
||||
// of another (for example, Search and Initializing search).
|
||||
return pairs.sort(function (a, b) { return b[0].length - a[0].length; });
|
||||
}
|
||||
|
||||
function translateChromeValue(value, pairs) {
|
||||
var trimmed = value.trim();
|
||||
if (!trimmed) return value;
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var from = pairs[i][0];
|
||||
var to = pairs[i][1];
|
||||
if (trimmed === from) {
|
||||
return value.slice(0, value.indexOf(trimmed)) + to + value.slice(value.indexOf(trimmed) + trimmed.length);
|
||||
}
|
||||
if (from.indexOf("#") !== -1) {
|
||||
var match = trimmed.match(new RegExp("^" + escapeRe(from).replace("#", "(.+?)") + "$"));
|
||||
if (match) {
|
||||
var rendered = to.replace("#", match[1]);
|
||||
return value.slice(0, value.indexOf(trimmed)) + rendered + value.slice(value.indexOf(trimmed) + trimmed.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function localizeTree(root, pairs) {
|
||||
if (!root) return;
|
||||
var element = root.nodeType === 1 ? root : root.parentElement;
|
||||
if (!element) return;
|
||||
|
||||
var attributed = [];
|
||||
if (element.matches && element.matches("[title],[aria-label],[placeholder]")) attributed.push(element);
|
||||
if (element.querySelectorAll) {
|
||||
attributed = attributed.concat(
|
||||
Array.prototype.slice.call(element.querySelectorAll("[title],[aria-label],[placeholder]"))
|
||||
);
|
||||
}
|
||||
for (var a = 0; a < attributed.length; a++) {
|
||||
["title", "aria-label", "placeholder"].forEach(function (name) {
|
||||
if (!attributed[a].hasAttribute(name)) return;
|
||||
var before = attributed[a].getAttribute(name);
|
||||
var after = translateChromeValue(before, pairs);
|
||||
if (after !== before) attributed[a].setAttribute(name, after);
|
||||
});
|
||||
}
|
||||
|
||||
var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
||||
var textNode;
|
||||
while ((textNode = walker.nextNode())) {
|
||||
var translated = translateChromeValue(textNode.nodeValue, pairs);
|
||||
if (translated !== textNode.nodeValue) textNode.nodeValue = translated;
|
||||
}
|
||||
}
|
||||
|
||||
function localizeRevisionDates(targetCode) {
|
||||
var strings = i18n.languages[targetCode];
|
||||
if (!strings || !window.Intl || !Intl.DateTimeFormat) return;
|
||||
var nodes = document.querySelectorAll(".git-revision-date-localized-plugin-date");
|
||||
var visibleFormat = new Intl.DateTimeFormat(strings.locale, {
|
||||
year: "numeric", month: "long", day: "numeric", timeZone: "UTC",
|
||||
});
|
||||
var titleFormat = new Intl.DateTimeFormat(strings.locale, {
|
||||
year: "numeric", month: "long", day: "numeric",
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||
timeZone: "UTC", timeZoneName: "short",
|
||||
});
|
||||
for (var d = 0; d < nodes.length; d++) {
|
||||
var original = nodes[d].getAttribute("title") || nodes[d].textContent;
|
||||
var match = original.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?/);
|
||||
if (!match) continue;
|
||||
var date = new Date(Date.UTC(
|
||||
Number(match[1]), Number(match[2]) - 1, Number(match[3]),
|
||||
Number(match[4] || 0), Number(match[5] || 0), Number(match[6] || 0)
|
||||
));
|
||||
nodes[d].textContent = visibleFormat.format(date);
|
||||
nodes[d].setAttribute("title", titleFormat.format(date));
|
||||
}
|
||||
}
|
||||
|
||||
function localizeChrome(targetCode) {
|
||||
if (targetCode === i18n.default || !i18n.languages[targetCode]) return;
|
||||
var pairs = translationPairs(targetCode);
|
||||
var roots = document.querySelectorAll(CHROME_SELECTOR);
|
||||
for (var r = 0; r < roots.length; r++) localizeTree(roots[r], pairs);
|
||||
localizeRevisionDates(targetCode);
|
||||
|
||||
// Search results, copy-button tooltips, and dialogs are populated after
|
||||
// load. Translate only newly-created theme chrome, never book content.
|
||||
if (!window.__siteI18nObserver && document.body) {
|
||||
window.__siteI18nObserver = new MutationObserver(function (mutations) {
|
||||
for (var m = 0; m < mutations.length; m++) {
|
||||
var changed = mutations[m].type === "characterData"
|
||||
? mutations[m].target.parentElement
|
||||
: mutations[m].target;
|
||||
if (!changed || !changed.closest || !changed.closest(CHROME_SELECTOR)) continue;
|
||||
localizeTree(changed, pairs);
|
||||
if (changed.closest(".md-source-file")) localizeRevisionDates(targetCode);
|
||||
}
|
||||
});
|
||||
window.__siteI18nObserver.observe(document.body, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["title", "aria-label", "placeholder"],
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function siteBasePath() {
|
||||
var p = location.pathname;
|
||||
try {
|
||||
var configured = new URL(window.SITE_ROOT).pathname.replace(/\/$/, "");
|
||||
// Use the configured deployment subpath when it is actually present.
|
||||
// Local preview servers commonly mount at /, so they fall through to
|
||||
// prefix discovery below instead of inheriting the production path.
|
||||
if (configured && configured !== "/" &&
|
||||
(p === configured || p.indexOf(configured + "/") === 0)) {
|
||||
return configured;
|
||||
}
|
||||
} catch (_) {}
|
||||
var best = -1;
|
||||
for (var code in cfg) {
|
||||
if (!cfg.hasOwnProperty(code)) continue;
|
||||
var idx = p.indexOf(cfg[code].prefix);
|
||||
if (idx !== -1 && (best === -1 || idx < best)) best = idx;
|
||||
}
|
||||
if (best !== -1) return p.slice(0, best);
|
||||
return "/";
|
||||
}
|
||||
|
||||
// ── sidebar rewriting (links + text) ──────────────────────
|
||||
|
||||
function rewriteSidebar(targetCode) {
|
||||
var target = cfg[targetCode];
|
||||
var strings = i18n.languages[targetCode];
|
||||
var defCode = null;
|
||||
for (var c in cfg) { if (cfg[c].default) { defCode = c; break; } }
|
||||
defCode = defCode || "zh";
|
||||
|
||||
var base = siteBasePath();
|
||||
if (base.charAt(base.length - 1) !== "/") base += "/";
|
||||
|
||||
var links = document.querySelectorAll(".md-nav__link");
|
||||
for (var i = 0; i < links.length; i++) {
|
||||
var el = links[i];
|
||||
var href = el.getAttribute("href");
|
||||
var navText = el.querySelector(".md-ellipsis");
|
||||
var currentText = navText ? navText.textContent.trim() : "";
|
||||
|
||||
if (href && href.charAt(0) !== "#") {
|
||||
// Resolve href to a clean path relative to docs root, then
|
||||
// translate it via the unified translatePath() function. This
|
||||
// handles prose links, experiment-index links, and the Chinese
|
||||
// default in one place — no scattered patches.
|
||||
try {
|
||||
var u = new URL(href, location.href);
|
||||
if (u.origin === location.origin) {
|
||||
var linkPath = u.pathname;
|
||||
if (linkPath.indexOf(base) === 0) {
|
||||
var linkRel = "/" + linkPath.slice(base.length).replace(/^\//, "");
|
||||
var linkLang = detectLang(linkRel);
|
||||
// The canonical sidebar is rendered from the default-language
|
||||
// nav, so an un-suffixed /chapterN/ link always starts as the
|
||||
// default experiment index. Do not let the remembered active
|
||||
// locale make it look pre-translated.
|
||||
if (/^\/chapter\d+\/?$/.test(linkRel)) {
|
||||
linkLang = defCode;
|
||||
if (targetCode !== defCode && !target.readmeSuffix) {
|
||||
// Do not advertise a translated experiment index that
|
||||
// does not exist yet. The chapter prose remains linked by
|
||||
// the parent entry and every visible link stays valid.
|
||||
var item = el.closest(".md-nav__item");
|
||||
if (item) item.hidden = true;
|
||||
}
|
||||
}
|
||||
var translated = translatePath(linkRel, linkLang, targetCode);
|
||||
if (translated) {
|
||||
el.setAttribute("href", base + translated.replace(/^\//, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (navText && strings.nav[currentText]) {
|
||||
navText.textContent = strings.nav[currentText];
|
||||
}
|
||||
}
|
||||
|
||||
// Translate the drawer's per-chapter sub-nav headers. When a chapter
|
||||
// subtree is opened on mobile, Material shows the chapter title again
|
||||
// as <label class="md-nav__title">第2章 …</label>; those labels are
|
||||
// plain text (not .md-nav__link), so the loop above misses them. The
|
||||
// site-name title at the top is not in the nav catalog and stays untouched.
|
||||
var subTitles = document.querySelectorAll(".md-sidebar--primary .md-nav__title");
|
||||
for (var st = 0; st < subTitles.length; st++) {
|
||||
var stNodes = subTitles[st].childNodes;
|
||||
for (var sn = 0; sn < stNodes.length; sn++) {
|
||||
var node = stNodes[sn];
|
||||
if (node.nodeType !== 3) continue;
|
||||
var key = node.textContent.trim();
|
||||
if (key && strings.nav[key]) {
|
||||
node.textContent = strings.nav[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
localizeChrome(targetCode);
|
||||
}
|
||||
|
||||
// ── language switch (the actual navigation) ──────────────
|
||||
|
||||
function applyDocumentLocale(code) {
|
||||
var strings = i18n.languages[code] || i18n.languages[i18n.default];
|
||||
document.documentElement.lang = strings.locale;
|
||||
document.documentElement.dir = strings.direction;
|
||||
if (document.body) document.body.setAttribute("dir", strings.direction);
|
||||
window.siteCurrentLanguage = code;
|
||||
}
|
||||
|
||||
function switchTo(target) {
|
||||
var rawPath = location.pathname;
|
||||
var basePath = siteBasePath();
|
||||
var cleanPath = "/" + rawPath.slice(basePath.length).replace(/^\//, "");
|
||||
var activeLang = detectLang(cleanPath);
|
||||
applyDocumentLocale(activeLang);
|
||||
if (!target || target === activeLang) return;
|
||||
var rel = translatePath(cleanPath, activeLang, target);
|
||||
if (!rel) return;
|
||||
var siteRoot = window.SITE_ROOT.replace(/\/$/, "") + "/";
|
||||
var finalUrl = siteRoot + rel.replace(/^\//, "");
|
||||
// Force a full page reload (bypass Material's navigation.instant, which
|
||||
// intercepts location.href and may bounce the user back). We're moving
|
||||
// to a different language edition, which is a different "site" — full
|
||||
// reload is the right semantic anyway.
|
||||
window.location.replace(finalUrl);
|
||||
}
|
||||
|
||||
// ── custom dropdown ─────────────────────────────────────
|
||||
|
||||
function closeDropdown(returnFocus) {
|
||||
var trigger = document.getElementById("lang-selector");
|
||||
var menu = document.getElementById("lang-menu");
|
||||
if (!trigger || !menu) return;
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
menu.hidden = true;
|
||||
if (returnFocus) trigger.focus();
|
||||
}
|
||||
|
||||
function openDropdown(focusDirection) {
|
||||
var trigger = document.getElementById("lang-selector");
|
||||
var menu = document.getElementById("lang-menu");
|
||||
if (!trigger || !menu) return;
|
||||
trigger.setAttribute("aria-expanded", "true");
|
||||
menu.hidden = false;
|
||||
|
||||
if (focusDirection) {
|
||||
var options = menu.querySelectorAll(".lang-menu__option");
|
||||
if (!options.length) return;
|
||||
var target = menu.querySelector('[aria-checked="true"]');
|
||||
if (focusDirection === "first") target = options[0];
|
||||
if (focusDirection === "last") target = options[options.length - 1];
|
||||
(target || options[0]).focus();
|
||||
}
|
||||
}
|
||||
|
||||
function moveMenuFocus(current, amount) {
|
||||
var menu = document.getElementById("lang-menu");
|
||||
if (!menu) return;
|
||||
var options = Array.prototype.slice.call(
|
||||
menu.querySelectorAll(".lang-menu__option")
|
||||
);
|
||||
if (!options.length) return;
|
||||
var currentIndex = options.indexOf(current);
|
||||
var nextIndex = (currentIndex + amount + options.length) % options.length;
|
||||
options[nextIndex].focus();
|
||||
}
|
||||
|
||||
function optionLocale(code) {
|
||||
return i18n.languages[code] ? i18n.languages[code].locale : code;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var rawPath = location.pathname;
|
||||
var basePath = siteBasePath();
|
||||
var cleanPath = "/" + rawPath.slice(basePath.length).replace(/^\//, "");
|
||||
var activeLang = detectLang(cleanPath);
|
||||
applyDocumentLocale(activeLang);
|
||||
|
||||
var trigger = document.getElementById("lang-selector");
|
||||
var menu = document.getElementById("lang-menu");
|
||||
if (!trigger || !menu) {
|
||||
// Localization is useful even if a downstream theme override removes
|
||||
// the selector itself. Do not make translated navigation depend on
|
||||
// that optional header control.
|
||||
rememberLang(activeLang);
|
||||
if (activeLang !== i18n.default) rewriteSidebar(activeLang);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build menu items on first sight of an empty dropdown.
|
||||
if (menu.children.length === 0) {
|
||||
var codes = Object.keys(cfg);
|
||||
for (var idx = 0; idx < codes.length; idx++) {
|
||||
var code = codes[idx];
|
||||
var option = document.createElement("button");
|
||||
option.type = "button";
|
||||
option.className = "lang-menu__option";
|
||||
option.setAttribute("role", "menuitemradio");
|
||||
option.setAttribute("data-lang-code", code);
|
||||
option.setAttribute(
|
||||
"aria-checked",
|
||||
code === activeLang ? "true" : "false"
|
||||
);
|
||||
option.setAttribute("tabindex", "-1");
|
||||
|
||||
var check = document.createElement("span");
|
||||
check.className = "lang-menu__check";
|
||||
check.setAttribute("aria-hidden", "true");
|
||||
|
||||
var label = document.createElement("span");
|
||||
label.className = "lang-menu__label";
|
||||
label.setAttribute("lang", optionLocale(code));
|
||||
label.setAttribute(
|
||||
"dir",
|
||||
i18n.languages[code].direction === "rtl" ? "rtl" : "auto"
|
||||
);
|
||||
label.textContent = cfg[code].label;
|
||||
|
||||
option.appendChild(check);
|
||||
option.appendChild(label);
|
||||
menu.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the trigger and checked item in sync after SPA navigation.
|
||||
var currentLabel = cfg[activeLang].label;
|
||||
var labelNode = trigger.querySelector("[data-lang-label]");
|
||||
if (labelNode) {
|
||||
labelNode.textContent = currentLabel;
|
||||
labelNode.setAttribute("lang", optionLocale(activeLang));
|
||||
labelNode.setAttribute(
|
||||
"dir",
|
||||
i18n.languages[activeLang].direction === "rtl" ? "rtl" : "auto"
|
||||
);
|
||||
}
|
||||
trigger.setAttribute(
|
||||
"aria-label",
|
||||
i18n.languages[activeLang].ui["select.language"] + ": " + currentLabel
|
||||
);
|
||||
|
||||
var options = menu.querySelectorAll(".lang-menu__option");
|
||||
for (var optionIndex = 0; optionIndex < options.length; optionIndex++) {
|
||||
var isActive =
|
||||
options[optionIndex].getAttribute("data-lang-code") === activeLang;
|
||||
options[optionIndex].setAttribute("aria-checked", isActive ? "true" : "false");
|
||||
}
|
||||
closeDropdown(false);
|
||||
|
||||
var defCode = null;
|
||||
for (var c in cfg) { if (cfg[c].default) { defCode = c; break; } }
|
||||
rememberLang(activeLang);
|
||||
if (activeLang !== (defCode || "zh")) {
|
||||
rewriteSidebar(activeLang);
|
||||
}
|
||||
}
|
||||
|
||||
// ── bootstrap ────────────────────────────────────────────
|
||||
|
||||
// Bind handlers once via event delegation so the dropdown keeps working
|
||||
// if Material re-creates the header during SPA navigation.
|
||||
if (!window.__langSwitcherBound) {
|
||||
window.__langSwitcherBound = true;
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!e.target || !e.target.closest) return;
|
||||
|
||||
var trigger = e.target.closest("#lang-selector");
|
||||
if (trigger) {
|
||||
var isOpen = trigger.getAttribute("aria-expanded") === "true";
|
||||
if (isOpen) closeDropdown(false);
|
||||
else openDropdown(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var option = e.target.closest(".lang-menu__option");
|
||||
if (option) {
|
||||
var targetCode = option.getAttribute("data-lang-code");
|
||||
closeDropdown(false);
|
||||
switchTo(targetCode);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.target.closest(".lang-switcher")) closeDropdown(false);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (!e.target || !e.target.closest) return;
|
||||
var trigger = e.target.closest("#lang-selector");
|
||||
if (trigger) {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
openDropdown(e.key === "ArrowDown" ? "first" : "last");
|
||||
} else if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
if (trigger.getAttribute("aria-expanded") === "true") {
|
||||
closeDropdown(false);
|
||||
} else {
|
||||
openDropdown("current");
|
||||
}
|
||||
} else if (e.key === "Escape") {
|
||||
closeDropdown(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var option = e.target.closest(".lang-menu__option");
|
||||
if (!option) return;
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
moveMenuFocus(option, e.key === "ArrowDown" ? 1 : -1);
|
||||
} else if (e.key === "Home" || e.key === "End") {
|
||||
e.preventDefault();
|
||||
openDropdown(e.key === "Home" ? "first" : "last");
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
closeDropdown(true);
|
||||
} else if (e.key === "Tab") {
|
||||
closeDropdown(false);
|
||||
} else if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
option.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exposed for extras/auto-translate.js, which routes the reader to the
|
||||
// source edition before overlaying machine translation on a language we
|
||||
// do not build. Deliberately limited to the two operations it needs
|
||||
// rather than exporting the whole module.
|
||||
window.langSwitcher = {
|
||||
// Language code of the edition the current URL belongs to.
|
||||
current: function () {
|
||||
return detectLang(cleanPathname());
|
||||
},
|
||||
// Absolute URL of this page in `code`, or null when the current page
|
||||
// has no counterpart there (or is already in that edition).
|
||||
urlFor: function (code) {
|
||||
var cleanPath = cleanPathname();
|
||||
var rel = translatePath(cleanPath, detectLang(cleanPath), code);
|
||||
if (!rel) return null;
|
||||
return (
|
||||
window.SITE_ROOT.replace(/\/$/, "") + "/" + rel.replace(/^\//, "")
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
function cleanPathname() {
|
||||
var basePath = siteBasePath();
|
||||
return "/" + location.pathname.slice(basePath.length).replace(/^\//, "");
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", render);
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
// Re-run on every Material SPA navigation. Material exposes document$
|
||||
// (a ReactiveSubscribable) that fires after each navigation.instant
|
||||
// page swap. Without this hook, the sidebar DOM gets re-rendered by
|
||||
// Material with the original (Chinese) nav text and we never get to
|
||||
// translate it for non-default languages.
|
||||
if (window.document$) {
|
||||
window.document$.subscribe(render);
|
||||
} else {
|
||||
// Fallback for older Material or other themes.
|
||||
document.addEventListener("locationchange", render);
|
||||
var _pushState = history.pushState;
|
||||
history.pushState = function () {
|
||||
_pushState.apply(this, arguments);
|
||||
setTimeout(render, 60);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
bindWhenReady();
|
||||
})();
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* MathJax 3 configuration for MkDocs Material.
|
||||
*
|
||||
* This file MUST be loaded before the MathJax bundle
|
||||
* (`tex-mml-chtml.js`) in mkdocs.yml `extra_javascript` — MathJax reads
|
||||
* `window.MathJax` once at startup, so setting it afterwards has no effect.
|
||||
*
|
||||
* `pymdownx.arithmatex` (generic mode) rewrites the book's `$...$` and
|
||||
* `$$...$$` source math into `\(...\)` / `\[...\]` wrapped in
|
||||
* `<span class="arithmatex">` / `<div class="arithmatex">`. MathJax's
|
||||
* default delimiters are `$...$`/`$$...$$`, which no longer exist in the
|
||||
* rendered HTML, so without this config nothing is typeset at all.
|
||||
*/
|
||||
window.MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['\\(', '\\)']],
|
||||
displayMath: [['\\[', '\\]']],
|
||||
processEscapes: true,
|
||||
processEnvironments: true,
|
||||
},
|
||||
options: {
|
||||
// Only typeset elements arithmatex marked as math; ignore everything
|
||||
// else (code blocks, search summaries, etc.).
|
||||
ignoreHtmlClass: '.*|',
|
||||
processHtmlClass: 'arithmatex',
|
||||
},
|
||||
};
|
||||
|
||||
// Material's `navigation.instant` swaps page content without a full reload,
|
||||
// so MathJax must re-typeset after every page swap (same pattern as
|
||||
// extras/mermaid-init.js).
|
||||
document$.subscribe(() => {
|
||||
MathJax.startup.output.clearCache();
|
||||
MathJax.typesetClear();
|
||||
MathJax.texReset();
|
||||
MathJax.typesetPromise();
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Mermaid loader for MkDocs Material.
|
||||
*
|
||||
* Material's `navigation.instant` swaps page content without a full reload,
|
||||
* so Mermaid must be (re-)initialized after every page swap. We hook into
|
||||
* both the initial load and Material's custom `document-subscriber` event.
|
||||
*
|
||||
* The mermaid.js runtime itself is loaded via mkdocs.yml `extra_javascript`
|
||||
* with `defer`, so it's available by the time this script runs.
|
||||
*/
|
||||
(function () {
|
||||
function isMermaidReady() {
|
||||
return typeof window.mermaid !== 'undefined';
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
if (!isMermaidReady()) return;
|
||||
try {
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
theme: document.documentElement.getAttribute('data-md-color-scheme') === 'slate'
|
||||
? 'dark'
|
||||
: 'default',
|
||||
securityLevel: 'loose', // allow $ in labels, e.g. $web_search
|
||||
flowchart: { curve: 'basis', useMaxWidth: true },
|
||||
});
|
||||
window.mermaid.run({ querySelector: '.mermaid:not([data-processed])' });
|
||||
} catch (e) {
|
||||
console.warn('[mermaid] render failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-render whenever Material swaps to a new page.
|
||||
document.addEventListener('DOMContentLoaded', renderAll);
|
||||
document$.subscribe(renderAll); // `document$` is provided by Material
|
||||
|
||||
// Re-theme when the user toggles light/dark.
|
||||
new MutationObserver(renderAll)
|
||||
.observe(document.documentElement,
|
||||
{ attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
||||
})();
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Desktop navigation controls for Material's primary sidebar.
|
||||
*
|
||||
* Background: with `navigation.sections` + `navigation.indexes`, each
|
||||
* chapter renders as
|
||||
*
|
||||
* <li class="md-nav__item--section md-nav__item--nested">
|
||||
* <input class="md-toggle" id="__nav_N">
|
||||
* <div class="md-nav__link md-nav__container">
|
||||
* <a href="…/chapterN/">chapter title</a> ← navigates
|
||||
* <label for="__nav_N">chevron</label> ← toggles
|
||||
* </div>
|
||||
* <nav class="md-nav">…配套实验…</nav>
|
||||
* </li>
|
||||
*
|
||||
* Material only honours the checkbox on mobile; on desktop the section
|
||||
* subtree is always visible and the chevron is hidden. We inject CSS so
|
||||
* that on desktop, too, the subtree follows the checkbox and the chevron
|
||||
* is visible/clickable. Material checks the active chapter's checkbox at
|
||||
* render time, so the default state (active chapter open, rest closed)
|
||||
* comes for free.
|
||||
*
|
||||
* One case needs JS: on pages Material doesn't consider "active" — the
|
||||
* translated editions (sidebar links are rewritten client-side by
|
||||
* lang-switcher.js) and the per-experiment pages, which aren't in the
|
||||
* nav — no checkbox is checked. There we match the current URL against
|
||||
* each section's chapter number and open the matching section.
|
||||
*
|
||||
* The header also contains a desktop-only button for hiding the entire
|
||||
* primary sidebar. Its state is persisted locally and restored across full
|
||||
* reloads as well as Material's instant page swaps.
|
||||
*
|
||||
* Re-runs on every Material page swap (navigation.instant) via document$.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var SIDEBAR_STORAGE_KEY = "ai-agent-book.primary-sidebar-collapsed";
|
||||
|
||||
function ensureStyle() {
|
||||
if (document.getElementById("nav-collapse-style")) return;
|
||||
var s = document.createElement("style");
|
||||
s.id = "nav-collapse-style";
|
||||
s.textContent = [
|
||||
"@media screen and (min-width: 76.25em) {",
|
||||
// Collapse the subtree when the checkbox is unchecked (Material
|
||||
// keeps section subtrees always-visible on desktop by default).
|
||||
" .md-sidebar--primary .md-nav__item--nested > .md-toggle:not(:checked) ~ .md-nav {",
|
||||
" display: none !important;",
|
||||
" }",
|
||||
// Material hides the section chevron on desktop; bring it back and
|
||||
// make it clickable.
|
||||
" .md-sidebar--primary .md-nav__item--nested > .md-nav__container > label.md-nav__link {",
|
||||
" display: flex;",
|
||||
" align-items: center;",
|
||||
" cursor: pointer;",
|
||||
" pointer-events: auto !important;",
|
||||
" margin: 0;",
|
||||
// 4px top padding centres the 1.2rem icon on the title's FIRST line
|
||||
// (6px link padding + ~1.3 line-height): the container aligns
|
||||
// flex-start (book-theme.css) so wrapped two-line titles don't pull
|
||||
// the chevron down between the lines.
|
||||
" padding: 4px 0.2rem 0 0.4rem;",
|
||||
" }",
|
||||
// Material's own stylesheet already rotates the chevron's ::after
|
||||
// by 90° when the checkbox is checked — no extra transform here.
|
||||
" .md-sidebar--primary .md-nav__item--nested > .md-nav__container > label.md-nav__link .md-nav__icon {",
|
||||
" display: block;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n");
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function applyDefaultState() {
|
||||
var sidebar = document.querySelector(".md-sidebar--primary");
|
||||
if (!sidebar) return;
|
||||
|
||||
// Material already checked a section's checkbox? Then its render-time
|
||||
// active detection worked — nothing to fix up.
|
||||
if (sidebar.querySelector(".md-nav__item--nested > .md-toggle:checked")) return;
|
||||
|
||||
// Otherwise (translated edition or a page outside the nav), derive the
|
||||
// chapter from the URL and open the matching section.
|
||||
var m = location.pathname.match(/chapter(\d+)/);
|
||||
if (!m) return;
|
||||
var wanted = m[1];
|
||||
var sections = sidebar.querySelectorAll(".md-nav__item--nested");
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var link = sections[i].querySelector(":scope > .md-nav__container > a.md-nav__link");
|
||||
var checkbox = sections[i].querySelector(":scope > .md-toggle");
|
||||
if (!link || !checkbox) continue;
|
||||
var lm = (link.getAttribute("href") || "").match(/chapter(\d+)/);
|
||||
if (lm && lm[1] === wanted) {
|
||||
checkbox.checked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readSidebarPreference() {
|
||||
try {
|
||||
var value = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
||||
return value === null ? null : value === "true";
|
||||
} catch (_) {
|
||||
// Storage may be disabled by the browser. The control still works for
|
||||
// the current page, and the root class survives Material page swaps.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeSidebarPreference(collapsed) {
|
||||
try {
|
||||
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed));
|
||||
} catch (_) {
|
||||
// A blocked localStorage must not prevent readers from using the
|
||||
// collapse control for the current page.
|
||||
}
|
||||
}
|
||||
|
||||
function setSidebarCollapsed(collapsed, remember) {
|
||||
document.documentElement.classList.toggle("sidebar-nav-collapsed", collapsed);
|
||||
|
||||
var button = document.querySelector("[data-sidebar-toggle]");
|
||||
if (button) {
|
||||
var catalog = window.SITE_I18N;
|
||||
var code = window.siteCurrentLanguage || (catalog && catalog.default) || "zh";
|
||||
var strings = catalog && catalog.languages && catalog.languages[code];
|
||||
var label = strings && strings.sidebar
|
||||
? strings.sidebar[collapsed ? "show" : "hide"]
|
||||
: (collapsed ? "展开侧边栏" : "隐藏侧边栏");
|
||||
button.setAttribute("aria-expanded", String(!collapsed));
|
||||
button.setAttribute("aria-label", label);
|
||||
button.setAttribute("title", label);
|
||||
}
|
||||
|
||||
if (remember) writeSidebarPreference(collapsed);
|
||||
}
|
||||
|
||||
function initSidebarToggle() {
|
||||
var button = document.querySelector("[data-sidebar-toggle]");
|
||||
var sidebar = document.querySelector(".md-sidebar--primary");
|
||||
if (!button || !sidebar || sidebar.hidden) {
|
||||
if (button) button.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect aria-controls to the sidebar generated by Material. The id is
|
||||
// re-applied because navigation.instant replaces page content in place.
|
||||
sidebar.id = "primary-navigation";
|
||||
button.hidden = false;
|
||||
|
||||
var preference = readSidebarPreference();
|
||||
var collapsed = preference === null
|
||||
? document.documentElement.classList.contains("sidebar-nav-collapsed")
|
||||
: preference;
|
||||
setSidebarCollapsed(collapsed, false);
|
||||
|
||||
if (button.getAttribute("data-sidebar-toggle-bound") !== "true") {
|
||||
button.setAttribute("data-sidebar-toggle-bound", "true");
|
||||
button.addEventListener("click", function () {
|
||||
var next = !document.documentElement.classList.contains("sidebar-nav-collapsed");
|
||||
setSidebarCollapsed(next, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
ensureStyle();
|
||||
applyDefaultState();
|
||||
initSidebarToggle();
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
if (window.document$) window.document$.subscribe(init);
|
||||
})();
|
||||
@@ -0,0 +1,89 @@
|
||||
// Point Material's search at this edition's slice of the search index.
|
||||
//
|
||||
// scripts/split_search_index.py splits the one 55 MB search_index.json the
|
||||
// search plugin emits into `search/search_index.<slug>.json`, one per book
|
||||
// edition (slug = the edition's URL directory: `book`, `book-en`, ...), each
|
||||
// carrying that edition plus the shared experiment pages. This script decides
|
||||
// which slice the current page needs and rewrites the request for it.
|
||||
//
|
||||
// Why an XHR patch rather than configuration: Material builds the index URL
|
||||
// as `new URL("search/search_index.json", config.base)` and requests it via
|
||||
// XMLHttpRequest while bundle.js initialises. There is no config seam for the
|
||||
// path, and `extra_javascript` files load *after* bundle.js — too late. So
|
||||
// overrides/main.html loads this file inside the `config` block, which base.html
|
||||
// renders immediately before bundle.js, and we intercept the one request.
|
||||
//
|
||||
// If anything here fails to identify an edition we simply leave the URL alone:
|
||||
// search_index.json still holds the default edition plus the shared pages, so
|
||||
// search degrades to its previous behaviour rather than breaking.
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var INDEX_RE = /\/search\/search_index\.json$/;
|
||||
|
||||
// `chapterN/README.<readmeSuffix>/` and `index.<code>/` — pages that belong
|
||||
// to an edition but live outside its book-*/ directory.
|
||||
var README_RE = /\/chapter\d+\/README\.([A-Za-z-]+)\/?(?:index\.html)?$/;
|
||||
var HOMEPAGE_RE = /\/index\.([A-Za-z-]+)\/?(?:index\.html)?$/;
|
||||
|
||||
// Mirrors edition_of() in scripts/split_search_index.py — keep the two in
|
||||
// step, or a reader gets an index that omits the edition they are reading.
|
||||
function slugForPath(path, cfg) {
|
||||
var code, entry;
|
||||
|
||||
// 1. An explicit book-*/ path segment. Exact segment matches only, so the
|
||||
// `/ai-agent-book/` site subpath can never be mistaken for an edition.
|
||||
var slugs = {};
|
||||
for (code in cfg) {
|
||||
if (cfg.hasOwnProperty(code) && cfg[code].prefix) {
|
||||
slugs[cfg[code].prefix.replace(/\/$/, "")] = true;
|
||||
}
|
||||
}
|
||||
var segments = path.split("/");
|
||||
for (var i = 0; i < segments.length; i++) {
|
||||
if (slugs[segments[i]]) return segments[i];
|
||||
}
|
||||
|
||||
// 2. Translated experiment indexes and homepages, keyed by their suffix.
|
||||
var match = path.match(README_RE) || path.match(HOMEPAGE_RE);
|
||||
if (match) {
|
||||
for (code in cfg) {
|
||||
if (!cfg.hasOwnProperty(code)) continue;
|
||||
entry = cfg[code];
|
||||
if (!entry.prefix) continue;
|
||||
if (entry.readmeSuffix === match[1] || code === match[1]) {
|
||||
return entry.prefix.replace(/\/$/, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Language-agnostic pages (the `chapterN/` experiments, the site root).
|
||||
// Reuse the switcher's remembered locale so a reader who came from an
|
||||
// edition keeps searching it; lang-switcher.js writes the same key.
|
||||
var remembered = null;
|
||||
try {
|
||||
remembered = sessionStorage.getItem("lang-switcher-active");
|
||||
} catch (_) {}
|
||||
if (remembered && cfg[remembered] && cfg[remembered].prefix) {
|
||||
return cfg[remembered].prefix.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var cfg = window.LANG_CONFIG;
|
||||
if (!cfg) return; // header.html emits it before this script; nothing to do.
|
||||
|
||||
var slug = slugForPath(location.pathname, cfg);
|
||||
if (!slug) return;
|
||||
|
||||
var open = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function (method, url) {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
if (typeof url === "string" && INDEX_RE.test(url)) {
|
||||
args[1] = url.replace(INDEX_RE, "/search/search_index." + slug + ".json");
|
||||
}
|
||||
return open.apply(this, args);
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,318 @@
|
||||
{
|
||||
"zh": {
|
||||
"material_locale": "zh",
|
||||
"nav": {
|
||||
"首页": "首页",
|
||||
"引言": "引言",
|
||||
"第1章 Agent基础知识": "第1章 Agent基础知识",
|
||||
"第2章 上下文工程": "第2章 上下文工程",
|
||||
"第3章 用户记忆和知识库": "第3章 用户记忆和知识库",
|
||||
"第4章 工具": "第4章 工具",
|
||||
"第5章 CodingAgent与通用Agent": "第5章 CodingAgent与通用Agent",
|
||||
"第6章 交互:观察与动作空间的扩展": "第6章 交互:观察与动作空间的扩展",
|
||||
"第7章 Agent的评估": "第7章 Agent的评估",
|
||||
"第8章 模型后训练": "第8章 模型后训练",
|
||||
"第9章 Agent的持续进化": "第9章 Agent的持续进化",
|
||||
"第10章 多Agent协作": "第10章 多Agent协作",
|
||||
"后记": "后记",
|
||||
"思考题参考答案": "思考题参考答案",
|
||||
"配套实验": "配套实验"
|
||||
},
|
||||
"sidebar": { "show": "展开侧边栏", "hide": "隐藏侧边栏" },
|
||||
"palette": { "light": "切换日间模式", "dark": "切换夜间模式" }
|
||||
},
|
||||
"zhtw": {
|
||||
"material_locale": "zh-TW",
|
||||
"nav": {
|
||||
"首页": "首頁",
|
||||
"引言": "引言",
|
||||
"第1章 Agent基础知识": "第 1 章 · Agent 基礎知識",
|
||||
"第2章 上下文工程": "第 2 章 · 上下文工程",
|
||||
"第3章 用户记忆和知识库": "第 3 章 · 使用者記憶和知識庫",
|
||||
"第4章 工具": "第 4 章 · 工具",
|
||||
"第5章 CodingAgent与通用Agent": "第 5 章 · Coding Agent 與程式碼生成",
|
||||
"第6章 交互:观察与动作空间的扩展": "第 6 章 · 互動:觀察與動作空間的擴展",
|
||||
"第7章 Agent的评估": "第 7 章 · Agent 的評估",
|
||||
"第8章 模型后训练": "第 8 章 · 模型後訓練",
|
||||
"第9章 Agent的持续进化": "第 9 章 · Agent 的持續進化",
|
||||
"第10章 多Agent协作": "第 10 章 · 多 Agent 協作",
|
||||
"后记": "後記",
|
||||
"思考题参考答案": "思考題參考答案",
|
||||
"配套实验": "配套實驗"
|
||||
},
|
||||
"sidebar": { "show": "展開側邊欄", "hide": "隱藏側邊欄" },
|
||||
"palette": { "light": "切換日間模式", "dark": "切換夜間模式" }
|
||||
},
|
||||
"en": {
|
||||
"material_locale": "en",
|
||||
"nav": {
|
||||
"首页": "Home",
|
||||
"引言": "Introduction",
|
||||
"第1章 Agent基础知识": "Chapter 1 · Getting Started with AI Agents",
|
||||
"第2章 上下文工程": "Chapter 2 · Context Engineering",
|
||||
"第3章 用户记忆和知识库": "Chapter 3 · User Memory & Knowledge Base",
|
||||
"第4章 工具": "Chapter 4 · Tools",
|
||||
"第5章 CodingAgent与通用Agent": "Chapter 5 · Coding Agent & Code Generation",
|
||||
"第6章 交互:观察与动作空间的扩展": "Chapter 6 · Interaction: Expanding the Observation and Action Spaces",
|
||||
"第7章 Agent的评估": "Chapter 7 · Evaluating Agents",
|
||||
"第8章 模型后训练": "Chapter 8 · Model Post-Training",
|
||||
"第9章 Agent的持续进化": "Chapter 9 · Continual Evolution of Agents",
|
||||
"第10章 多Agent协作": "Chapter 10 · Multi-Agent Collaboration",
|
||||
"后记": "Afterword",
|
||||
"思考题参考答案": "Reference Answers",
|
||||
"配套实验": "Experiments"
|
||||
},
|
||||
"sidebar": { "show": "Show navigation", "hide": "Hide navigation" },
|
||||
"palette": { "light": "Switch to light mode", "dark": "Switch to dark mode" }
|
||||
},
|
||||
"es": {
|
||||
"material_locale": "es",
|
||||
"nav": {
|
||||
"首页": "Inicio",
|
||||
"引言": "Introducción",
|
||||
"第1章 Agent基础知识": "Capítulo 1 · Fundamentos de los Agentes de IA",
|
||||
"第2章 上下文工程": "Capítulo 2 · Ingeniería de Contexto",
|
||||
"第3章 用户记忆和知识库": "Capítulo 3 · Memoria de Usuario y Base de Conocimiento",
|
||||
"第4章 工具": "Capítulo 4 · Herramientas",
|
||||
"第5章 CodingAgent与通用Agent": "Capítulo 5 · Agente de Código y Generación de Código",
|
||||
"第6章 交互:观察与动作空间的扩展": "Capítulo 6 · Interacción: la expansión de los espacios de observación y de acción",
|
||||
"第7章 Agent的评估": "Capítulo 7 · Evaluación de Agentes",
|
||||
"第8章 模型后训练": "Capítulo 8 · Posentrenamiento de Modelos",
|
||||
"第9章 Agent的持续进化": "Capítulo 9 · Evolución Continua de los Agentes",
|
||||
"第10章 多Agent协作": "Capítulo 10 · Colaboración Multiagente",
|
||||
"后记": "Epílogo",
|
||||
"思考题参考答案": "Respuestas de Referencia",
|
||||
"配套实验": "Experimentos"
|
||||
},
|
||||
"sidebar": { "show": "Mostrar navegación", "hide": "Ocultar navegación" },
|
||||
"palette": { "light": "Cambiar al modo claro", "dark": "Cambiar al modo oscuro" }
|
||||
},
|
||||
"id": {
|
||||
"material_locale": "id",
|
||||
"nav": {
|
||||
"首页": "Beranda",
|
||||
"引言": "Pendahuluan",
|
||||
"第1章 Agent基础知识": "Bab 1 · Dasar-dasar Agent",
|
||||
"第2章 上下文工程": "Bab 2 · Rekayasa Konteks",
|
||||
"第3章 用户记忆和知识库": "Bab 3 · Memori Pengguna dan Basis Pengetahuan",
|
||||
"第4章 工具": "Bab 4 · Alat",
|
||||
"第5章 CodingAgent与通用Agent": "Bab 5 · Coding Agent dan Pembuatan Kode",
|
||||
"第6章 交互:观察与动作空间的扩展": "Bab 6 · Interaksi: Perluasan Ruang Observasi dan Ruang Aksi",
|
||||
"第7章 Agent的评估": "Bab 7 · Evaluasi Agent",
|
||||
"第8章 模型后训练": "Bab 8 · Pascapelatihan Model",
|
||||
"第9章 Agent的持续进化": "Bab 9 · Evolusi Berkelanjutan Agent",
|
||||
"第10章 多Agent协作": "Bab 10 · Kolaborasi Multi-Agent",
|
||||
"后记": "Penutup",
|
||||
"思考题参考答案": "Jawaban Referensi",
|
||||
"配套实验": "Eksperimen"
|
||||
},
|
||||
"sidebar": { "show": "Tampilkan navigasi", "hide": "Sembunyikan navigasi" },
|
||||
"palette": { "light": "Beralih ke mode terang", "dark": "Beralih ke mode gelap" }
|
||||
},
|
||||
"ru": {
|
||||
"material_locale": "ru",
|
||||
"nav": {
|
||||
"首页": "Главная",
|
||||
"引言": "Введение",
|
||||
"第1章 Agent基础知识": "Глава 1 · Введение в ИИ-агенты",
|
||||
"第2章 上下文工程": "Глава 2 · Инженерия контекста",
|
||||
"第3章 用户记忆和知识库": "Глава 3 · Память и база знаний",
|
||||
"第4章 工具": "Глава 4 · Инструменты",
|
||||
"第5章 CodingAgent与通用Agent": "Глава 5 · Кодинг-агент и генерация кода",
|
||||
"第6章 交互:观察与动作空间的扩展": "Глава 6 · Взаимодействие: расширение пространства наблюдений и пространства действий",
|
||||
"第7章 Agent的评估": "Глава 7 · Оценка агентов",
|
||||
"第8章 模型后训练": "Глава 8 · Постобучение модели",
|
||||
"第9章 Agent的持续进化": "Глава 9 · Непрерывная эволюция агентов",
|
||||
"第10章 多Agent协作": "Глава 10 · Мультиагентное взаимодействие",
|
||||
"后记": "Послесловие",
|
||||
"思考题参考答案": "Ответы к вопросам",
|
||||
"配套实验": "Эксперименты"
|
||||
},
|
||||
"sidebar": { "show": "Показать навигацию", "hide": "Скрыть навигацию" },
|
||||
"palette": { "light": "Включить светлый режим", "dark": "Включить тёмный режим" }
|
||||
},
|
||||
"ta": {
|
||||
"material_locale": "ta",
|
||||
"nav": {
|
||||
"首页": "முகப்பு",
|
||||
"引言": "அறிமுகம்",
|
||||
"第1章 Agent基础知识": "அத்தியாயம் 1 · AI ஏஜெண்ட் அடிப்படைகள்",
|
||||
"第2章 上下文工程": "அத்தியாயம் 2 · சூழல் பொறியியல்",
|
||||
"第3章 用户记忆和知识库": "அத்தியாயம் 3 · பயனர் நினைவகம் மற்றும் அறிவுத்தளம்",
|
||||
"第4章 工具": "அத்தியாயம் 4 · கருவிகள்",
|
||||
"第5章 CodingAgent与通用Agent": "அத்தியாயம் 5 · குறியீட்டு ஏஜெண்ட் மற்றும் குறியீடு உருவாக்கம்",
|
||||
"第6章 交互:观察与动作空间的扩展": "அத்தியாயம் 6 · தொடர்பாடல்: அவதானிப்பு மற்றும் செயல் வெளிகளின் விரிவாக்கம்",
|
||||
"第7章 Agent的评估": "அத்தியாயம் 7 · ஏஜெண்ட் மதிப்பீடு",
|
||||
"第8章 模型后训练": "அத்தியாயம் 8 · மாதிரி பிந்தைய பயிற்சி",
|
||||
"第9章 Agent的持续进化": "அத்தியாயம் 9 · ஏஜெண்டின் தொடர்ச்சியான பரிணாமம்",
|
||||
"第10章 多Agent协作": "அத்தியாயம் 10 · பல-ஏஜெண்ட் கூட்டுச் செயல்பாடு",
|
||||
"后记": "பின்னுரை",
|
||||
"思考题参考答案": "குறிப்புப் பதில்கள்",
|
||||
"配套实验": "சோதனைகள்"
|
||||
},
|
||||
"sidebar": { "show": "வழிசெலுத்தலைக் காட்டு", "hide": "வழிசெலுத்தலை மறை" },
|
||||
"palette": { "light": "ஒளி பயன்முறைக்கு மாறு", "dark": "இருண்ட பயன்முறைக்கு மாறு" }
|
||||
},
|
||||
"vi": {
|
||||
"material_locale": "vi",
|
||||
"ui_overrides": {
|
||||
"search.result.initializer": "Đang khởi tạo tìm kiếm",
|
||||
"source.file.contributors": "Người đóng góp",
|
||||
"tabs": "Thẻ"
|
||||
},
|
||||
"nav": {
|
||||
"首页": "Trang chủ",
|
||||
"引言": "Giới thiệu",
|
||||
"第1章 Agent基础知识": "Chương 1 · Nền tảng AI Agent",
|
||||
"第2章 上下文工程": "Chương 2 · Kỹ thuật ngữ cảnh",
|
||||
"第3章 用户记忆和知识库": "Chương 3 · Bộ nhớ và Cơ sở kiến thức",
|
||||
"第4章 工具": "Chương 4 · Công cụ",
|
||||
"第5章 CodingAgent与通用Agent": "Chương 5 · Coding Agent và Tạo mã",
|
||||
"第6章 交互:观察与动作空间的扩展": "Chương 6 · Tương tác: mở rộng không gian quan sát và không gian hành động",
|
||||
"第7章 Agent的评估": "Chương 7 · Đánh giá Agent",
|
||||
"第8章 模型后训练": "Chương 8 · Post-training mô hình",
|
||||
"第9章 Agent的持续进化": "Chương 9 · Sự tiến hóa liên tục của Agent",
|
||||
"第10章 多Agent协作": "Chương 10 · Cộng tác đa Agent",
|
||||
"后记": "Lời bạt",
|
||||
"思考题参考答案": "Đáp án tham khảo",
|
||||
"配套实验": "Thí nghiệm"
|
||||
},
|
||||
"sidebar": { "show": "Hiện thanh điều hướng", "hide": "Ẩn thanh điều hướng" },
|
||||
"palette": { "light": "Chuyển sang chế độ sáng", "dark": "Chuyển sang chế độ tối" }
|
||||
},
|
||||
"ja": {
|
||||
"material_locale": "ja",
|
||||
"nav": {
|
||||
"首页": "ホーム",
|
||||
"引言": "はじめに",
|
||||
"第1章 Agent基础知识": "第1章 · AI Agent 入門",
|
||||
"第2章 上下文工程": "第2章 · コンテキストエンジニアリング",
|
||||
"第3章 用户记忆和知识库": "第3章 · ユーザーメモリと知識ベース",
|
||||
"第4章 工具": "第4章 · ツール",
|
||||
"第5章 CodingAgent与通用Agent": "第5章 · Coding Agent とコード生成",
|
||||
"第6章 交互:观察与动作空间的扩展": "第6章 · 交互:観察空間と動作空間の拡張",
|
||||
"第7章 Agent的评估": "第7章 · Agent の評価",
|
||||
"第8章 模型后训练": "第8章 · モデルのポストトレーニング",
|
||||
"第9章 Agent的持续进化": "第9章 · Agent の継続的進化",
|
||||
"第10章 多Agent协作": "第10章 · マルチ Agent 協調",
|
||||
"后记": "あとがき",
|
||||
"思考题参考答案": "演習問題の解答例",
|
||||
"配套实验": "実験"
|
||||
},
|
||||
"sidebar": { "show": "ナビゲーションを表示", "hide": "ナビゲーションを隠す" },
|
||||
"palette": { "light": "ライトモードに切り替え", "dark": "ダークモードに切り替え" }
|
||||
},
|
||||
"ko": {
|
||||
"material_locale": "ko",
|
||||
"nav": {
|
||||
"首页": "홈",
|
||||
"引言": "들어가며",
|
||||
"第1章 Agent基础知识": "제1장 · AI 에이전트 기초",
|
||||
"第2章 上下文工程": "제2장 · 컨텍스트 엔지니어링",
|
||||
"第3章 用户记忆和知识库": "제3장 · 사용자 메모리와 지식 베이스",
|
||||
"第4章 工具": "제4장 · 도구",
|
||||
"第5章 CodingAgent与通用Agent": "제5장 · 코딩 에이전트와 코드 생성",
|
||||
"第6章 交互:观察与动作空间的扩展": "제6장 · 상호작용: 관찰 공간과 행동 공간의 확장",
|
||||
"第7章 Agent的评估": "제7장 · 에이전트 평가",
|
||||
"第8章 模型后训练": "제8장 · 모델 사후 학습",
|
||||
"第9章 Agent的持续进化": "제9장 · 에이전트의 지속적 진화",
|
||||
"第10章 多Agent协作": "제10장 · 멀티 에이전트 협업",
|
||||
"后记": "맺음말",
|
||||
"思考题参考答案": "생각해 볼 문제 참고 답안",
|
||||
"配套实验": "실습"
|
||||
},
|
||||
"sidebar": { "show": "탐색 메뉴 표시", "hide": "탐색 메뉴 숨기기" },
|
||||
"palette": { "light": "라이트 모드로 전환", "dark": "다크 모드로 전환" }
|
||||
},
|
||||
"ar": {
|
||||
"material_locale": "ar",
|
||||
"nav": {
|
||||
"首页": "الرئيسية",
|
||||
"引言": "المقدمة",
|
||||
"第1章 Agent基础知识": "الفصل 1 · أساسيات الوكلاء",
|
||||
"第2章 上下文工程": "الفصل 2 · هندسة السياق",
|
||||
"第3章 用户记忆和知识库": "الفصل 3 · ذاكرة المستخدم وقاعدة المعرفة",
|
||||
"第4章 工具": "الفصل 4 · الأدوات",
|
||||
"第5章 CodingAgent与通用Agent": "الفصل 5 · وكيل البرمجة وتوليد الشيفرة",
|
||||
"第6章 交互:观察与动作空间的扩展": "الفصل 6 · التفاعل: توسيع فضاء الملاحظة وفضاء الفعل",
|
||||
"第7章 Agent的评估": "الفصل 7 · تقييم الوكلاء",
|
||||
"第8章 模型后训练": "الفصل 8 · ما بعد تدريب النموذج",
|
||||
"第9章 Agent的持续进化": "الفصل 9 · التطور المستمر للوكلاء",
|
||||
"第10章 多Agent协作": "الفصل 10 · التعاون متعدد الوكلاء",
|
||||
"后记": "الخاتمة",
|
||||
"思考题参考答案": "إجابات الأسئلة",
|
||||
"配套实验": "التجارب"
|
||||
},
|
||||
"sidebar": { "show": "إظهار التنقل", "hide": "إخفاء التنقل" },
|
||||
"palette": { "light": "التبديل إلى الوضع الفاتح", "dark": "التبديل إلى الوضع الداكن" }
|
||||
},
|
||||
"tr": {
|
||||
"material_locale": "tr",
|
||||
"nav": {
|
||||
"首页": "Ana Sayfa",
|
||||
"引言": "Giriş",
|
||||
"第1章 Agent基础知识": "Bölüm 1 · Yapay Zeka Ajanlarına Giriş",
|
||||
"第2章 上下文工程": "Bölüm 2 · Context Engineering",
|
||||
"第3章 用户记忆和知识库": "Bölüm 3 · Kullanıcı Belleği ve Bilgi Tabanı",
|
||||
"第4章 工具": "Bölüm 4 · Araçlar",
|
||||
"第5章 CodingAgent与通用Agent": "Bölüm 5 · Kodlama Agent'ı ve Kod Üretimi",
|
||||
"第6章 交互:观察与动作空间的扩展": "Bölüm 6 · Etkileşim: Gözlem ve Eylem Uzaylarının Genişletilmesi",
|
||||
"第7章 Agent的评估": "Bölüm 7 · Agent'ın Değerlendirmesi",
|
||||
"第8章 模型后训练": "Bölüm 8 · Model Post-Training",
|
||||
"第9章 Agent的持续进化": "Bölüm 9 · Agent'ın Sürekli Evrimi",
|
||||
"第10章 多Agent协作": "Bölüm 10 · Çoklu Agent İş Birliği",
|
||||
"后记": "Sonsöz",
|
||||
"思考题参考答案": "Düşünce Soruları — Örnek Cevaplar",
|
||||
"配套实验": "Deneyler"
|
||||
},
|
||||
"sidebar": { "show": "Navigasyonu göster", "hide": "Navigasyonu gizle" },
|
||||
"palette": { "light": "Açık moda geç", "dark": "Koyu moda geç" }
|
||||
},
|
||||
"hu": {
|
||||
"material_locale": "hu",
|
||||
"ui_overrides": {
|
||||
"source": "Ugrás a kódtárhoz"
|
||||
},
|
||||
"nav": {
|
||||
"首页": "Kezdőlap",
|
||||
"引言": "Bevezetés",
|
||||
"第1章 Agent基础知识": "1. fejezet · Ismerkedés az AI-ügynökökkel",
|
||||
"第2章 上下文工程": "2. fejezet · Kontextustervezés",
|
||||
"第3章 用户记忆和知识库": "3. fejezet · Felhasználói memória és tudásbázis",
|
||||
"第4章 工具": "4. fejezet · Eszközök",
|
||||
"第5章 CodingAgent与通用Agent": "5. fejezet · Kódoló ágens és kódgenerálás",
|
||||
"第6章 交互:观察与动作空间的扩展": "6. fejezet · Interakció: a megfigyelési és a cselekvési tér kiterjesztése",
|
||||
"第7章 Agent的评估": "7. fejezet · Ügynökök kiértékelése",
|
||||
"第8章 模型后训练": "8. fejezet · Modell poszt-tréning",
|
||||
"第9章 Agent的持续进化": "9. fejezet · Az ágensek folyamatos evolúciója",
|
||||
"第10章 多Agent协作": "10. fejezet · Többügynökös együttműködés",
|
||||
"后记": "Utószó",
|
||||
"思考题参考答案": "Válaszvázlatok a gondolatébresztő kérdésekhez",
|
||||
"配套实验": "Kísérletek"
|
||||
},
|
||||
"sidebar": { "show": "Navigáció megjelenítése", "hide": "Navigáció elrejtése" },
|
||||
"palette": { "light": "Váltás világos módra", "dark": "Váltás sötét módra" }
|
||||
},
|
||||
"he": {
|
||||
"material_locale": "he",
|
||||
"nav": {
|
||||
"首页": "דף הבית",
|
||||
"引言": "הקדמה",
|
||||
"第1章 Agent基础知识": "פרק 1 · צעדים ראשונים עם סוכני AI",
|
||||
"第2章 上下文工程": "פרק 2 · הנדסת הקשר",
|
||||
"第3章 用户记忆和知识库": "פרק 3 · זיכרון משתמש ובסיס ידע",
|
||||
"第4章 工具": "פרק 4 · כלים",
|
||||
"第5章 CodingAgent与通用Agent": "פרק 5 · סוכן קוד ויצירת קוד",
|
||||
"第6章 交互:观察与动作空间的扩展": "פרק 6 · אינטראקציה: הרחבת מרחבי התצפית והפעולה",
|
||||
"第7章 Agent的评估": "פרק 7 · הערכת סוכנים",
|
||||
"第8章 模型后训练": "פרק 8 · אימון־על של מודלים",
|
||||
"第9章 Agent的持续进化": "פרק 9 · התפתחות מתמשכת של סוכנים",
|
||||
"第10章 多Agent协作": "פרק 10 · שיתוף פעולה רב־סוכני",
|
||||
"后记": "אחרית דבר",
|
||||
"思考题参考答案": "תשובות לשאלות למחשבה",
|
||||
"配套实验": "ניסויים נלווים"
|
||||
},
|
||||
"sidebar": { "show": "הצגת ניווט", "hide": "הסתרת ניווט" },
|
||||
"palette": { "light": "מעבר למצב בהיר", "dark": "מעבר למצב כהה" }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user