Files
brainwave/prototype/app.js
T

1273 lines
55 KiB
JavaScript
Raw Normal View History

2026-08-04 17:54:15 +08:00
(() => {
"use strict";
const SAMPLE_QUESTION = "我是否要接受这次工作调整?怎样做能更稳妥?";
const SAMPLE_LINES = [9, 8, 8, 8, 8, 8];
2026-08-04 23:00:05 +08:00
const SAMPLE_HISTORY = [
{
id: "sample-1",
date: "今天 · 21:10",
lines: [9, 8, 8, 8, 8, 8],
question: SAMPLE_QUESTION,
explanationSource: "AI 生成",
action: "写下三条已知事实,再约一次不超过 20 分钟的信息沟通。"
},
{
id: "sample-2",
date: "昨天 · 07:40",
lines: [8, 8, 7, 8, 8, 8],
question: null,
explanationSource: "本地解读",
action: null
},
{
id: "sample-3",
date: "7 月 30 日 · 22:15",
lines: [7, 7, 8, 8, 7, 6],
question: "这段合作应该怎样调整边界?",
explanationSource: null,
action: "先把需要确认的职责写成一页清单。"
}
];
2026-08-04 17:54:15 +08:00
const HEXAGRAM_NAMES = [
"", "乾", "坤", "屯", "蒙", "需", "讼", "师", "比", "小畜", "履", "泰", "否",
"同人", "大有", "谦", "豫", "随", "蛊", "临", "观", "噬嗑", "贲", "剥", "复",
"无妄", "大畜", "颐", "大过", "坎", "离", "咸", "恒", "遁", "大壮", "晋",
"明夷", "家人", "睽", "蹇", "解", "损", "益", "夬", "姤", "萃", "升", "困",
"井", "革", "鼎", "震", "艮", "渐", "归妹", "丰", "旅", "巽", "兑", "涣",
"节", "中孚", "小过", "既济", "未济"
];
// 行为下卦、列为上卦;三位编码均按自下而上排列,1 为阳、0 为阴。
const KING_WEN = {
"111": { "111": 1, "100": 34, "010": 5, "001": 26, "000": 11, "011": 9, "101": 14, "110": 43 },
"100": { "111": 25, "100": 51, "010": 3, "001": 27, "000": 24, "011": 42, "101": 21, "110": 17 },
"010": { "111": 6, "100": 40, "010": 29, "001": 4, "000": 7, "011": 59, "101": 64, "110": 47 },
"001": { "111": 33, "100": 62, "010": 39, "001": 52, "000": 15, "011": 53, "101": 56, "110": 31 },
"000": { "111": 12, "100": 16, "010": 8, "001": 23, "000": 2, "011": 20, "101": 35, "110": 45 },
"011": { "111": 44, "100": 32, "010": 48, "001": 18, "000": 46, "011": 57, "101": 50, "110": 28 },
"101": { "111": 13, "100": 55, "010": 63, "001": 22, "000": 36, "011": 37, "101": 30, "110": 49 },
"110": { "111": 10, "100": 54, "010": 60, "001": 41, "000": 19, "011": 61, "101": 38, "110": 58 }
};
const icons = {
lock: '<svg aria-hidden="true" viewBox="0 0 24 24"><rect x="5" y="10" width="14" height="10" rx="2"/><path d="M8.5 10V7.5a3.5 3.5 0 017 0V10"/></svg>',
arrow: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M5 12h14M14 7l5 5-5 5"/></svg>',
chevron: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M9 6l6 6-6 6"/></svg>',
2026-08-04 23:00:05 +08:00
shield: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M12 3l7 3v5c0 4.7-2.8 8-7 10-4.2-2-7-5.3-7-10V6l7-3z"/><path d="M9.2 12l1.8 1.8 3.9-4"/></svg>',
book: '<svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 5.5A2.5 2.5 0 016.5 3H11v16H6.5A2.5 2.5 0 004 21.5v-16zM20 5.5A2.5 2.5 0 0017.5 3H13v16h4.5a2.5 2.5 0 012.5 2.5v-16z"/></svg>'
2026-08-04 17:54:15 +08:00
};
const screen = document.querySelector("#screen");
const modalRoot = document.querySelector("#modal-root");
const toastRegion = document.querySelector("#toast-region");
const backButton = document.querySelector("[data-action='back']");
const state = {
screen: "welcome",
question: "",
lines: [],
coins: [null, null, null],
modal: null,
consent: false,
explanationSource: "ai",
2026-08-04 23:00:05 +08:00
aiError: false,
historyRecords: [],
selectedRecordId: null,
autoSaveHistory: true,
saveQuestion: true,
saveExplanation: true,
saveAction: true,
saved: false,
currentRecordId: "current-session",
currentSessionOptedOut: false,
currentSaveMode: null,
explanationGenerated: false,
questionOrigin: "welcome",
settingsOrigin: "home",
deleteReturnScreen: "history"
2026-08-04 17:54:15 +08:00
};
2026-08-04 23:00:05 +08:00
function sampleHistory() {
return SAMPLE_HISTORY.map((record) => ({ ...record, lines: [...record.lines] }));
}
2026-08-04 17:54:15 +08:00
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function lineIsYang(value) {
return value === 7 || value === 9;
}
function lineIsMoving(value) {
return value === 6 || value === 9;
}
function transformLines(lines) {
return lines.map((value) => {
if (value === 6) return 7;
if (value === 9) return 8;
return value;
});
}
function lookupHexagram(lines) {
const bits = lines.map((value) => lineIsYang(value) ? "1" : "0");
const lower = bits.slice(0, 3).join("");
const upper = bits.slice(3, 6).join("");
const number = KING_WEN[lower][upper];
return { number, name: HEXAGRAM_NAMES[number] };
}
function lineName(value, index) {
const position = ["初", "二", "三", "四", "五", "上"][index];
const polarity = lineIsYang(value) ? "九" : "六";
if (index === 0) return `${position}${polarity}`;
if (index === 5) return `${position}${polarity}`;
return `${polarity}${position}`;
}
function resultData() {
const sourceLines = state.lines.length === 6 ? state.lines : SAMPLE_LINES;
const changedLines = transformLines(sourceLines);
return {
sourceLines,
changedLines,
primary: lookupHexagram(sourceLines),
changed: lookupHexagram(changedLines),
movingIndexes: sourceLines
.map((value, index) => lineIsMoving(value) ? index : -1)
.filter((index) => index >= 0)
};
}
2026-08-04 23:00:05 +08:00
function hasSavableQuestion() {
return Boolean(state.question && !state.question.startsWith("未记录具体问题"));
}
function generatedExplanationSource() {
if (!state.explanationGenerated || !state.saveExplanation) return null;
return state.explanationSource === "ai" ? "AI 生成" : "本地解读";
}
function currentSessionRecord() {
return state.historyRecords.find((record) => record.id === state.currentRecordId);
}
function persistCurrentSession(mode = "manual") {
if (state.lines.length !== 6) return;
const data = resultData();
const existing = currentSessionRecord();
const record = {
id: state.currentRecordId,
date: "刚刚",
lines: [...data.sourceLines],
question: state.saveQuestion && hasSavableQuestion() ? state.question : null,
explanationSource: generatedExplanationSource(),
action: state.explanationGenerated && state.saveAction
? "写下三条已知事实,再进行一次短沟通。"
: null
};
if (existing) Object.assign(existing, record);
else state.historyRecords.unshift(record);
state.saved = true;
state.currentSessionOptedOut = false;
state.currentSaveMode = mode;
}
function attachExplanationToCurrentSession() {
const record = currentSessionRecord();
if (!record || !state.saved) return;
if (state.saveExplanation) record.explanationSource = generatedExplanationSource();
if (state.saveAction) {
record.action = "写下三条已知事实,再进行一次短沟通。";
}
}
function prepareNewSession() {
state.currentRecordId = `current-session-${Date.now()}-${state.historyRecords.length}`;
state.saved = false;
state.currentSessionOptedOut = false;
state.currentSaveMode = null;
state.explanationGenerated = false;
state.aiError = false;
}
2026-08-04 17:54:15 +08:00
function stepper(current, total) {
const progress = Math.round((current / total) * 100);
return `
<div class="stepper" aria-label="第 ${current} 步,共 ${total} 步">
<span>${String(current).padStart(2, "0")}</span>
<span class="stepper__track" style="--progress:${progress}%"></span>
<span>${String(total).padStart(2, "0")}</span>
</div>`;
}
function welcomeView() {
return `
<section class="page page--welcome" aria-labelledby="welcome-title">
<div>
<p class="eyebrow">慢一点,看清当下</p>
<h1 id="welcome-title">把一个犹豫,安静地放在这里。</h1>
<p class="lede">用你手中的三枚硬币,依次记录六次结果。这里提供文化文本与反思线索,不替你预测,也不替你决定。</p>
<div class="hero-mark" aria-hidden="true">
<span class="hero-mark__dot">观</span>
</div>
<ul class="principles" aria-label="产品原则">
<li>手动记录</li>
<li>本地成卦</li>
<li>选择解读</li>
</ul>
</div>
<div class="bottom-action">
<button class="button button--primary" type="button" data-action="begin">
写下此刻所问 ${icons.arrow}
</button>
<p class="fine-print" style="margin:0;text-align:center">工作名“一问”仅用于本轮视觉评审</p>
</div>
</section>`;
}
2026-08-04 23:00:05 +08:00
function historyRecordData(record) {
const changedLines = transformLines(record.lines);
const movingIndexes = record.lines
.map((value, index) => lineIsMoving(value) ? index : -1)
.filter((index) => index >= 0);
return {
primary: lookupHexagram(record.lines),
changed: lookupHexagram(changedLines),
changedLines,
movingIndexes
};
}
function recordRoute(record) {
const data = historyRecordData(record);
return data.movingIndexes.length
? `${data.primary.name} <span>之</span> ${data.changed.name}`
: data.primary.name;
}
function recentRecordCard(record) {
const data = historyRecordData(record);
return `
<button class="recent-card" type="button" data-action="open-history-detail" data-record-id="${record.id}">
<span class="recent-card__top">
<span>${record.date}</span>
<span class="source-badge">最近一次</span>
</span>
<span class="recent-card__route">${recordRoute(record)}</span>
<span class="recent-card__question">${escapeHtml(record.question ?? "未保存问题原文")}</span>
<span class="recent-card__meta">
<span>${data.movingIndexes.length ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span>
<span>${record.explanationSource ?? "仅保存卦象"}</span>
${icons.chevron}
</span>
</button>`;
}
function homeView() {
const latest = state.historyRecords[0];
return `
<section class="page page--home" aria-labelledby="home-title">
<header class="home-intro">
<p class="eyebrow">慢一点,看清当下</p>
<h1 id="home-title">此刻,有什么想安静看清?</h1>
<p class="lede">你投币,应用记录;结果用于整理想法,不替你预测或决定。</p>
<button class="button button--primary home-primary" type="button" data-action="begin">
开始一问 ${icons.arrow}
</button>
</header>
<section class="home-shelf" aria-labelledby="reflection-shelf-title">
<div class="section-heading home-shelf__heading">
<h2 id="reflection-shelf-title">你的观照</h2>
<button class="text-link" type="button" data-action="open-history">查看问卦簿</button>
</div>
${latest ? recentRecordCard(latest) : `
<button class="home-empty-card" type="button" data-action="open-history">
<span class="home-empty-card__icon">${icons.book}</span>
<span><strong>问卦簿尚无记录</strong><small>${state.autoSaveHistory ? "完成起卦后会默认保存在本机" : "自动保存已关闭,可在结果页保存本次"}</small></span>
${icons.chevron}
</button>`}
<aside class="local-data-card" data-local-data-notice>
<span class="local-data-card__icon">${icons.lock}</span>
<div>
<strong>起卦与历史默认保存在本机,不主动上传。</strong>
<p>只有你选择 AI 解读时,本次所需内容才会发送。</p>
<button class="text-link" type="button" data-action="open-settings">管理保存设置</button>
</div>
</aside>
</section>
</section>`;
}
function historyCard(record) {
const data = historyRecordData(record);
return `
<li>
<button class="history-card" type="button" data-action="open-history-detail" data-record-id="${record.id}">
<span class="history-card__top">
<span>${record.date}</span>
<span>${data.movingIndexes.length ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span>
</span>
<span class="history-card__route">${recordRoute(record)}</span>
<span class="history-card__question">${escapeHtml(record.question ?? "未保存问题原文")}</span>
<span class="history-card__footer">
<span>${record.explanationSource ?? "仅保存卦象"}</span>
${icons.chevron}
</span>
</button>
</li>`;
}
function historyView() {
const hasRecords = state.historyRecords.length > 0;
return `
<section class="page page--compact page--history" aria-labelledby="history-title">
<p class="eyebrow">本机记录</p>
<div class="history-title-row">
<h1 id="history-title">问卦簿</h1>
${hasRecords ? `<span class="source-badge">${state.historyRecords.length} 条</span>` : ""}
</div>
<p class="lede">回看当时看见了什么,而不是用旧结果替今天做决定。</p>
<div class="privacy-strip">
${icons.lock}
<span>记录只在本机,默认排除系统备份与设备迁移;你可随时删除。</span>
</div>
${hasRecords ? `
<ol class="history-list" data-history-list>
${state.historyRecords.map(historyCard).join("")}
</ol>` : `
<div class="history-empty" data-history-empty>
<span class="history-empty__mark" aria-hidden="true"><i></i><i></i></span>
<h2>还没有保存的记录</h2>
<p>${state.autoSaveHistory ? "完成一次起卦后,会默认把问题、卦象与后续解读保存在本机。" : "自动保存已经关闭;完成后仍可在结果页选择保存本次。"}</p>
<button class="text-link" type="button" data-action="open-settings">管理保存设置</button>
</div>`}
<div class="bottom-action">
<button class="button button--primary" type="button" data-action="begin">
${hasRecords ? "开始新的一问" : "开始第一次记录"} ${icons.arrow}
</button>
</div>
</section>`;
}
function settingRow({ action, title, description, checked, disabled = false, detail = false }) {
return `
<label class="setting-row${detail ? " setting-row--detail" : ""}${disabled ? " setting-row--disabled" : ""}">
<span class="setting-row__copy"><strong>${title}</strong><small>${description}</small></span>
<input
class="switch-control"
type="checkbox"
role="switch"
data-action="${action}"
${checked ? "checked" : ""}
${disabled ? "disabled" : ""}
/>
</label>`;
}
function settingsView() {
const detailsDisabled = !state.autoSaveHistory;
return `
<section class="page page--compact page--settings" aria-labelledby="settings-title">
<p class="eyebrow">本机与隐私</p>
<h1 id="settings-title">保存设置</h1>
<p class="lede">默认留住完整的反思过程;你可以全局关闭,也可以只调整保存内容。</p>
<div class="settings-boundary">
${icons.shield}
<div>
<strong>只保存在 App 私有存储</strong>
<p>历史不会主动上传,并默认排除系统备份、设备迁移与云同步。</p>
</div>
</div>
<section class="settings-card" aria-label="历史保存选项">
${settingRow({
action: "auto-save-toggle",
title: "自动保存完整记录",
description: state.autoSaveHistory ? "已开启 · 完成起卦后立即保存到本机" : "已关闭 · 结果页仍可手动保存本次",
checked: state.autoSaveHistory
})}
<div class="settings-card__details${detailsDisabled ? " settings-card__details--disabled" : ""}">
${settingRow({
action: "save-question-setting",
title: "保存问题原文",
description: "与你写下的卦象背景一同回顾",
checked: state.saveQuestion,
disabled: detailsDisabled,
detail: true
})}
${settingRow({
action: "save-explanation-setting",
title: "保存解读全文",
description: "本地或 AI 解读会加入同一次记录",
checked: state.saveExplanation,
disabled: detailsDisabled,
detail: true
})}
${settingRow({
action: "save-action-setting",
title: "保存行动记录",
description: "保留“可以试的一小步”与完成状态",
checked: state.saveAction,
disabled: detailsDisabled,
detail: true
})}
</div>
</section>
<p class="settings-ai-note">这些开关只控制本机历史,不会允许或触发 AI。每次 AI 解读仍需要你主动选择并确认发送范围。</p>
<section class="settings-danger" aria-labelledby="history-management-title">
<div>
<h2 id="history-management-title">管理本机记录</h2>
<p>当前原型会话中有 ${state.historyRecords.length} 条记录。</p>
</div>
<button class="button button--danger-quiet" type="button" data-action="open-clear-all" ${state.historyRecords.length ? "" : "disabled"}>清空全部问卦记录</button>
</section>
</section>`;
}
function historyDetailView() {
const record = state.historyRecords.find((item) => item.id === state.selectedRecordId);
if (!record) return historyView();
const data = historyRecordData(record);
const hasMoving = data.movingIndexes.length > 0;
return `
<article class="page page--compact page--history-detail" aria-labelledby="history-detail-title">
<header class="history-detail-header">
<p class="eyebrow">${record.date}</p>
<h1 id="history-detail-title">${recordRoute(record)}</h1>
<p>保存的起卦快照</p>
</header>
<div class="result-hero history-result-hero">
<div class="result-kicker"><span>coin-v1 · 本地快照</span><span>${hasMoving ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span></div>
<div class="hexagram-pair${hasMoving ? "" : " hexagram-pair--single"}">
${hexagramFigure(record.lines, data.primary, true)}
${hasMoving ? `<div class="transform-arrow" aria-label="变化为">之</div>${hexagramFigure(data.changedLines, data.changed, false)}` : ""}
</div>
</div>
<section class="content-section" aria-labelledby="saved-question-title">
<div class="section-heading"><h2 id="saved-question-title">当时所问</h2><span class="source-badge">${record.question ? "已保存" : "未保存"}</span></div>
<div class="content-card"><p>${escapeHtml(record.question ?? "这条记录没有保存问题原文。卦象结果和方法版本仍可独立复核。")}</p></div>
</section>
<section class="content-section" aria-labelledby="saved-content-title">
<div class="section-heading"><h2 id="saved-content-title">保存内容</h2></div>
<dl class="record-facts">
<div><dt>起卦结果</dt><dd>${data.primary.name}${hasMoving ? `之${data.changed.name}` : ""} · ${data.movingIndexes.length ? data.movingIndexes.map((index) => lineName(record.lines[index], index)).join("、") : "无动爻"}</dd></div>
<div><dt>解释来源</dt><dd>${record.explanationSource ?? "未保存解释"}</dd></div>
<div><dt>版本</dt><dd>coin-v1 · content-demo</dd></div>
</dl>
</section>
${record.action ? `
<section class="content-section" aria-labelledby="saved-action-title">
<div class="small-step"><div class="small-step__label" id="saved-action-title">当时记下的一小步</div><p>${escapeHtml(record.action)}</p></div>
</section>` : ""}
<div class="detail-danger-zone">
<button class="button button--danger-quiet" type="button" data-action="delete-record">删除这条记录</button>
<p>删除前会再次确认;原型刷新后恢复演示数据。</p>
</div>
</article>`;
}
2026-08-04 17:54:15 +08:00
function questionView() {
return `
<section class="page" aria-labelledby="question-title">
${stepper(1, 2)}
<p class="eyebrow">起念</p>
<h1 id="question-title">你想看清什么?</h1>
<p class="lede">写成一个开放的问题,比只问“会不会”更容易照见可行动的部分。</p>
<label class="field-label" for="question-input">此刻的问题</label>
<textarea
id="question-input"
class="question-input"
maxlength="120"
placeholder="例如:面对这次工作调整,我怎样做能更稳妥?"
data-question-input
>${escapeHtml(state.question)}</textarea>
<div class="field-meta">
2026-08-04 23:00:05 +08:00
<span>${state.autoSaveHistory ? "完成起卦后默认保存在本机" : "自动保存已关闭,本次可稍后手动保存"}</span>
2026-08-04 17:54:15 +08:00
<span data-char-count>${state.question.length}/120</span>
</div>
<div class="privacy-note">
${icons.lock}
<p>起卦计算在本地完成。只有你主动选择 AI 解读并再次确认后,所需内容才会被发送。</p>
</div>
<button class="text-link" type="button" data-action="skip-question">不写具体内容,直接开始</button>
<div class="bottom-action">
<button class="button button--primary" type="button" data-action="start-casting" ${state.question.trim() ? "" : "disabled"}>
开始记录六爻 ${icons.arrow}
</button>
</div>
</section>`;
}
function lineStack(lines) {
return [5, 4, 3, 2, 1, 0].map((index) => {
const value = lines[index];
if (!value) {
return '<div class="line-slot line-slot--empty" aria-label="尚未记录"></div>';
}
const kind = lineIsYang(value) ? "yang" : "yin";
const moving = lineIsMoving(value) ? " line-slot--moving" : "";
return `<div class="line-slot line-slot--${kind}${moving}" aria-label="${lineName(value, index)},数值 ${value}${lineIsMoving(value) ? ",动爻" : ""}">
${lineIsMoving(value) ? `<span class="line-slot__number">${value}</span>` : ""}
</div>`;
}).join("");
}
function coinButton(value, index) {
const face = value === 2 ? "字" : value === 3 ? "背" : "点选";
const score = value ? String(value) : "字 ↔ 背";
const valueAttr = value ? `data-value="${value}"` : "";
const next = value === null ? "字,2 分" : value === 2 ? "背,3 分" : "字,2 分";
return `
<div class="coin-wrap">
<button
class="coin"
type="button"
data-action="toggle-coin"
data-index="${index}"
${valueAttr}
aria-label="第 ${index + 1} 枚硬币,当前${value ? `${face},${score} 分` : "未记录"};点击切换为${next}"
>
<span class="coin__value">
<span class="coin__face">${face}</span>
<span class="coin__score">${score}</span>
</span>
</button>
<span>硬币 ${index + 1}</span>
</div>`;
}
function castingView() {
const round = state.lines.length + 1;
const selected = state.coins.every((value) => value !== null);
const sum = selected ? state.coins.reduce((total, value) => total + value, 0) : null;
const currentType = sum
? ({ 6: "老阴 · 动", 7: "少阳", 8: "少阴", 9: "老阳 · 动" })[sum]
: "";
return `
<section class="page page--compact" aria-labelledby="casting-title">
${stepper(2, 2)}
<div class="question-chip" title="${escapeHtml(state.question)}">${escapeHtml(state.question)}</div>
<div class="casting-layout">
<div class="line-progress-card">
<div class="line-progress-copy">
<strong>${state.lines.length} / 6</strong>
<p>从下往上记录</p>
${state.lines.length ? '<button class="text-link" type="button" data-action="undo-line">撤销上一爻</button>' : ""}
</div>
<div class="line-stack" aria-label="已记录的六爻">${lineStack(state.lines)}</div>
</div>
<div class="coin-prompt">
<h2 id="casting-title">第 ${round} 爻</h2>
<p>依照手中硬币,逐枚点选“字”或“背”</p>
</div>
<div class="coins" aria-label="三枚硬币录入">
${state.coins.map(coinButton).join("")}
</div>
<div class="round-sum" aria-live="polite">
${sum ? `本次合计 <strong>${sum}</strong> · ${currentType}` : "点按硬币可在“字 2”与“背 3”间切换"}
</div>
</div>
<div class="bottom-action">
<button class="button button--primary" type="button" data-action="confirm-line" ${selected ? "" : "disabled"}>
${round === 6 ? "记录并查看结果" : `确认第 ${round} 爻`} ${icons.arrow}
</button>
</div>
</section>`;
}
function hexLine(value, index, showMoving) {
const kind = lineIsYang(value) ? "yang" : "yin";
const moving = showMoving && lineIsMoving(value) ? " hex-line--moving" : "";
return `<div class="hex-line hex-line--${kind}${moving}" aria-label="${lineName(value, index)}${moving ? ",动爻" : ""}"><span></span><span></span></div>`;
}
function hexagramFigure(lines, hexagram, showMoving) {
const rendered = [5, 4, 3, 2, 1, 0]
.map((index) => hexLine(lines[index], index, showMoving))
.join("");
return `
<div class="hexagram-view">
<div class="hexagram-lines" role="img" aria-label="第 ${hexagram.number} 卦,${hexagram.name}卦">${rendered}</div>
<div class="hexagram-name">${hexagram.name}</div>
<div class="hexagram-number">第 ${hexagram.number} 卦</div>
</div>`;
}
function resultCopy(data) {
if (data.primary.number === 24) {
return {
classic: "复:亨。出入无疾,朋来无咎。反复其道,七日来复。利有攸往。",
plain: "“复”提醒人把注意力放回可返回、可重新开始之处。它不是替你判断成败,而是邀请你看见:此刻是否有一条更朴素、更合乎本心的路。",
moving: "初九:不远复,无祇悔,元吉。",
movingPlain: "偏离尚不远时就察觉并回转,代价通常较小。先修正一个最近的动作,比一次解决所有问题更可行。"
};
}
return {
classic: `${data.primary.name}卦 · 第 ${data.primary.number} 卦。本原型仅演示排盘与阅读层级,正式经典文本将从经校勘的本地内容包读取。`,
plain: "先观察卦象呈现的张力与变化,再回到你能核实的事实、感受和资源。不要把符号当成替你下结论的证据。",
moving: data.movingIndexes.length ? `${data.movingIndexes.map((index) => lineName(data.sourceLines[index], index)).join("、")}发生变化。` : "",
movingPlain: "变化的位置可作为反思入口:哪里正在松动,哪里仍需要保留余地?"
};
}
function errorCard() {
return `
<div class="error-card" role="alert">
<h3>AI 解读暂时没有返回</h3>
<p class="quiet">起卦结果仍完整保存在本页。你可以改看本地解读,或稍后重试。</p>
<div class="error-actions">
<button class="button button--secondary" type="button" data-action="choose-local">本地解读</button>
<button class="button button--danger-quiet" type="button" data-action="retry-ai">重试</button>
</div>
</div>`;
}
2026-08-04 23:00:05 +08:00
function currentSavePanel({ compact = false } = {}) {
if (state.saved) {
const explanationAttached = state.explanationGenerated && state.saveExplanation;
const savedContext = state.saveQuestion && hasSavableQuestion() ? "问题、卦象" : "卦象";
return `
<div class="current-save-panel${compact ? " current-save-panel--compact" : ""}" data-current-save-status>
<div class="current-save-panel__status">
<span class="current-save-panel__icon">${icons.book}</span>
<span>
<strong>${explanationAttached ? "解读已加入本次记录" : state.currentSaveMode === "automatic" ? "已自动保存到本机" : "已保存到本机问卦簿"}</strong>
<small>${explanationAttached ? `与${savedContext}归在同一次观照中` : `${savedContext}快照已进入问卦簿`}</small>
</span>
</div>
<div class="current-save-panel__actions">
<button class="text-link" type="button" data-action="open-history">查看问卦簿</button>
<button class="text-link text-link--danger" type="button" data-action="remove-current-record">本次不保存</button>
</div>
</div>`;
}
return `
<div class="current-save-panel current-save-panel--off${compact ? " current-save-panel--compact" : ""}" data-current-save-status>
<div class="current-save-panel__status">
<span class="current-save-panel__icon">${icons.lock}</span>
<span>
<strong>本次未保存</strong>
<small>${state.currentSessionOptedOut ? "你已选择不保留这次记录" : "自动保存已关闭;不会写入问卦簿"}</small>
</span>
</div>
<button class="button button--secondary" type="button" data-action="save-current">保存本次</button>
</div>`;
}
2026-08-04 17:54:15 +08:00
function resultView() {
const data = resultData();
const copy = resultCopy(data);
const hasMoving = data.movingIndexes.length > 0;
const question = state.question || "未记录具体问题";
return `
<section class="page page--compact" aria-labelledby="result-title">
<div class="result-hero">
<div class="result-kicker">
<span>起卦结果 · 本地计算</span>
<span>${hasMoving ? `${data.movingIndexes.length} 个动爻` : "无动爻"}</span>
</div>
<div class="hexagram-pair${hasMoving ? "" : " hexagram-pair--single"}">
${hexagramFigure(data.sourceLines, data.primary, true)}
${hasMoving ? `<div class="transform-arrow" aria-label="变化为">之</div>${hexagramFigure(data.changedLines, data.changed, false)}` : ""}
</div>
${hasMoving ? "" : '<div class="static-indicator">卦象稳定 · 不显示之卦</div>'}
</div>
<div class="content-section">
<div class="section-heading">
<h2 id="result-title">你所问</h2>
<span class="source-badge">本次记录</span>
</div>
<p class="question-chip" style="white-space:normal">${escapeHtml(question)}</p>
</div>
${state.aiError ? errorCard() : ""}
<div class="content-section">
<div class="section-heading">
<h2>经典原文</h2>
<span class="source-badge">经典文本</span>
</div>
<blockquote class="classic-quote">${copy.classic}</blockquote>
</div>
<div class="content-section">
<div class="section-heading">
<h2>本地白话</h2>
<span class="source-badge">本地内容</span>
</div>
<div class="plain-copy"><p>${copy.plain}</p></div>
</div>
${hasMoving ? `
<div class="content-section">
<div class="section-heading"><h2>动爻</h2><span class="source-badge">变化处</span></div>
<div class="moving-card">
<div class="moving-card__label">${data.movingIndexes.map((index) => lineName(data.sourceLines[index], index)).join(" · ")}</div>
<h3>${copy.moving}</h3>
<p class="quiet">${copy.movingPlain}</p>
</div>
</div>` : ""}
<hr class="divider" />
2026-08-04 23:00:05 +08:00
<div class="result-secondary-actions">
${currentSavePanel()}
<button class="text-link" type="button" data-action="restart">重新起一卦</button>
</div>
2026-08-04 17:54:15 +08:00
<div class="bottom-action">
<button class="button button--primary" type="button" data-action="open-interpretation">
解 · 选择一种解读方式 ${icons.arrow}
</button>
</div>
</section>`;
}
function loadingView() {
return `
<section class="page" aria-labelledby="loading-title">
<div class="loading-state">
<div class="loading-orbit" aria-hidden="true"></div>
<h1 id="loading-title" style="font-size:27px">正在整理反思线索</h1>
<p class="lede">结果已经生成。AI 只在此处协助组织文字,不参与起卦。</p>
</div>
</section>`;
}
function explanationView() {
const data = resultData();
const ai = state.explanationSource === "ai";
return `
<article class="page page--compact" aria-labelledby="explanation-title">
<header class="interpretation-header">
<p class="eyebrow">${ai ? "辅助反思" : "离线阅读"}</p>
<h1 id="explanation-title">从“回到起点”开始</h1>
<div class="interpretation-meta">
<span class="source-badge">${ai ? "AI 生成" : "本地解读"}</span>
<span>${data.primary.name}卦 · 第 ${data.primary.number} 卦</span>
</div>
</header>
<p class="interpretation-lead">眼前更重要的,也许不是立刻选定一条路,而是辨认哪一步能让你重新获得稳定与判断力。</p>
<section class="content-section" aria-labelledby="observation-title">
<div class="section-heading"><h2 id="observation-title">可以先观察</h2></div>
<ul class="reflection-list">
<li>你对“调整”的担心里,哪些是已经发生的事实,哪些是尚未验证的推测?</li>
<li>复卦的动处在最初一爻,适合把注意力放在能尽早修正的小偏差。</li>
<li>之卦为坤,提示行动时给现实条件、协作关系与节奏留出空间。</li>
</ul>
</section>
<section class="content-section" aria-labelledby="questions-title">
<div class="section-heading"><h2 id="questions-title">问问自己</h2></div>
<ol class="question-list">
<li>如果暂时不追求一次做对,我最想先确认的一个事实是什么?</li>
<li>哪一种选择更容易保留回转余地,而不是把自己锁死?</li>
<li>我可以向谁说明顾虑,并获得一条具体信息?</li>
</ol>
</section>
<section class="content-section" aria-labelledby="step-title">
<div class="small-step">
<div class="small-step__label" id="step-title">可以试的一小步</div>
<p>在今天结束前,写下“已知事实、尚待确认、我的底线”各一条,再约一次不超过 20 分钟的信息沟通。</p>
</div>
</section>
<p class="safety-note">这是一种文化反思与文字整理,不是预测、诊断或替代你的判断。涉及医疗、法律、财务或人身安全时,请寻求相应专业支持。</p>
<div class="bottom-action">
2026-08-04 23:00:05 +08:00
${currentSavePanel({ compact: true })}
<button class="button button--quiet" type="button" data-action="back-to-result">回到起卦结果</button>
2026-08-04 17:54:15 +08:00
</div>
</article>`;
}
2026-08-04 23:00:05 +08:00
function deleteRecordModal() {
return `
<div class="modal-backdrop" data-action="dismiss-modal">
<section class="modal-sheet modal-sheet--dialog" role="alertdialog" aria-modal="true" aria-labelledby="delete-modal-title" aria-describedby="delete-modal-description" data-modal-sheet>
<div class="modal-handle" aria-hidden="true"></div>
<header class="modal-header">
<div>
<h2 id="delete-modal-title">删除这条记录?</h2>
<p id="delete-modal-description">卦象、问题、已保存解读和行动记录会一并从本机删除。</p>
</div>
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</header>
<div class="notice notice--danger">
${icons.shield}<p>原型使用演示数据;正式版删除不可恢复,因此必须在这里再次确认。</p>
</div>
<div class="modal-actions">
<button class="button button--danger" type="button" data-action="confirm-delete">确认删除</button>
<button class="button button--secondary" type="button" data-action="close-modal">取消</button>
</div>
</section>
</div>`;
}
function clearAllModal() {
return `
<div class="modal-backdrop" data-action="dismiss-modal">
<section class="modal-sheet modal-sheet--dialog" role="alertdialog" aria-modal="true" aria-labelledby="clear-modal-title" aria-describedby="clear-modal-description" data-modal-sheet>
<div class="modal-handle" aria-hidden="true"></div>
<header class="modal-header">
<div>
<h2 id="clear-modal-title">清空全部记录?</h2>
<p id="clear-modal-description">当前 ${state.historyRecords.length} 条记录的卦象、问题、解读和行动内容都会从本机删除。</p>
</div>
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</header>
<div class="notice notice--danger">
${icons.shield}<p>这项操作不可恢复,但不会改变你的自动保存设置。</p>
</div>
<div class="modal-actions">
<button class="button button--danger" type="button" data-action="confirm-clear-all">确认清空全部</button>
<button class="button button--secondary" type="button" data-action="close-modal">取消</button>
</div>
</section>
</div>`;
}
2026-08-04 17:54:15 +08:00
function interpretationModal() {
return `
<div class="modal-backdrop" data-action="dismiss-modal">
<section class="modal-sheet" role="dialog" aria-modal="true" aria-labelledby="interpretation-modal-title" data-modal-sheet>
<div class="modal-handle" aria-hidden="true"></div>
<header class="modal-header">
<div>
<h2 id="interpretation-modal-title">选择解读方式</h2>
<p>起卦结果不会改变,只切换阅读材料的来源。</p>
</div>
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</header>
<div class="option-list">
<button class="option-card" type="button" data-action="choose-local">
<span class="option-card__icon" aria-hidden="true">本</span>
<span><strong>先看本地解读</strong><small>离线可用,不发送任何内容</small></span>
${icons.chevron}
</button>
<button class="option-card" type="button" data-action="choose-ai">
<span class="option-card__icon" aria-hidden="true">解</span>
<span><strong>使用 AI 辅助解读</strong><small>确认发送范围后再开始</small></span>
${icons.chevron}
</button>
</div>
</section>
</div>`;
}
function consentModal() {
return `
<div class="modal-backdrop" data-action="dismiss-modal">
<section class="modal-sheet" role="dialog" aria-modal="true" aria-labelledby="consent-modal-title" data-modal-sheet>
<div class="modal-handle" aria-hidden="true"></div>
<header class="modal-header">
<div>
<h2 id="consent-modal-title">发送前,请你确认</h2>
<p>AI 不参与起卦,只根据既有结果整理反思线索。</p>
</div>
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</header>
<div class="notice">
${icons.shield}
<p>将发送以下内容;不会发送设备标识、历史记录或其他本地数据。</p>
</div>
<ul class="data-list">
<li>你本次写下的问题</li>
<li>本卦、之卦与动爻结果</li>
<li>用于约束语气和安全边界的提示</li>
</ul>
<label class="consent-check">
<input type="checkbox" data-action="consent-toggle" ${state.consent ? "checked" : ""} />
<span>我已了解发送范围,并同意仅为本次 AI 解读使用这些内容。</span>
</label>
<div class="modal-actions">
<button class="button button--primary" type="button" data-action="confirm-ai" ${state.consent ? "" : "disabled"}>同意并开始解读</button>
<button class="button button--quiet" type="button" data-action="choose-local">改看本地解读</button>
</div>
</section>
</div>`;
}
function methodModal() {
return `
<div class="modal-backdrop" data-action="dismiss-modal">
<section class="modal-sheet" role="dialog" aria-modal="true" aria-labelledby="method-modal-title" data-modal-sheet>
<div class="modal-handle" aria-hidden="true"></div>
<header class="modal-header">
<div>
<h2 id="method-modal-title">三枚硬币法</h2>
<p>原型采用明确、可复核的固定规则。</p>
</div>
<button class="icon-button" type="button" data-action="close-modal" aria-label="关闭"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
</header>
<ol class="method-steps">
<li>准备三枚硬币,在现实中投掷一次。</li>
<li>在页面逐枚记录字面或背面,确认后形成一爻。</li>
<li>从下往上重复六次;程序只换算,不替你随机生成。</li>
</ol>
<div class="score-table" aria-label="硬币计分规则">
<div><strong>字 · 2</strong><span>字面记两分</span></div>
<div><strong>背 · 3</strong><span>背面记三分</span></div>
</div>
<p class="fine-print">合计 6 为老阴、7 为少阳、8 为少阴、9 为老阳;6 与 9 是动爻。</p>
<button class="button button--secondary" style="width:100%" type="button" data-action="close-modal">知道了</button>
</section>
</div>`;
}
function renderModal() {
if (state.modal === "interpretation") modalRoot.innerHTML = interpretationModal();
else if (state.modal === "consent") modalRoot.innerHTML = consentModal();
else if (state.modal === "method") modalRoot.innerHTML = methodModal();
2026-08-04 23:00:05 +08:00
else if (state.modal === "delete") modalRoot.innerHTML = deleteRecordModal();
else if (state.modal === "clear-all") modalRoot.innerHTML = clearAllModal();
2026-08-04 17:54:15 +08:00
else modalRoot.innerHTML = "";
document.body.style.overflow = state.modal ? "hidden" : "";
if (state.modal) {
window.setTimeout(() => {
modalRoot.querySelector("button, input")?.focus();
}, 20);
}
}
function render(options = {}) {
if (state.screen === "welcome") screen.innerHTML = welcomeView();
2026-08-04 23:00:05 +08:00
else if (state.screen === "home") screen.innerHTML = homeView();
else if (state.screen === "history") screen.innerHTML = historyView();
else if (state.screen === "history-detail") screen.innerHTML = historyDetailView();
else if (state.screen === "settings") screen.innerHTML = settingsView();
2026-08-04 17:54:15 +08:00
else if (state.screen === "question") screen.innerHTML = questionView();
else if (state.screen === "casting") screen.innerHTML = castingView();
else if (state.screen === "result") screen.innerHTML = resultView();
else if (state.screen === "loading") screen.innerHTML = loadingView();
else if (state.screen === "explanation") screen.innerHTML = explanationView();
2026-08-04 23:00:05 +08:00
backButton.hidden = state.screen === "welcome" || state.screen === "home";
2026-08-04 17:54:15 +08:00
renderModal();
if (!options.preserveFocus) {
screen.focus({ preventScroll: true });
window.scrollTo({ top: 0, behavior: "auto" });
}
}
function showToast(message) {
toastRegion.innerHTML = `<div class="toast">${escapeHtml(message)}</div>`;
window.setTimeout(() => {
toastRegion.innerHTML = "";
}, 1800);
}
function closeModal() {
state.modal = null;
state.consent = false;
renderModal();
}
function goBack() {
if (state.modal) {
closeModal();
return;
}
2026-08-04 23:00:05 +08:00
if (state.screen === "question") state.screen = state.questionOrigin;
else if (state.screen === "history") state.screen = "home";
else if (state.screen === "history-detail") state.screen = "history";
else if (state.screen === "settings") state.screen = state.settingsOrigin;
2026-08-04 17:54:15 +08:00
else if (state.screen === "casting") state.screen = "question";
else if (state.screen === "result") state.screen = "question";
else if (state.screen === "loading" || state.screen === "explanation") state.screen = "result";
render();
}
function beginAiExplanation() {
state.modal = null;
state.screen = "loading";
state.explanationSource = "ai";
render();
window.setTimeout(() => {
if (state.screen !== "loading") return;
2026-08-04 23:00:05 +08:00
state.explanationGenerated = true;
attachExplanationToCurrentSession();
2026-08-04 17:54:15 +08:00
state.screen = "explanation";
render();
}, 900);
}
document.addEventListener("input", (event) => {
if (!event.target.matches("[data-question-input]")) return;
state.question = event.target.value;
document.querySelector("[data-char-count]").textContent = `${state.question.length}/120`;
const startButton = document.querySelector("[data-action='start-casting']");
if (startButton) startButton.disabled = !state.question.trim();
});
document.addEventListener("change", (event) => {
2026-08-04 23:00:05 +08:00
if (event.target.matches("[data-action='consent-toggle']")) {
state.consent = event.target.checked;
const confirm = document.querySelector("[data-action='confirm-ai']");
if (confirm) confirm.disabled = !state.consent;
} else if (event.target.matches("[data-action='auto-save-toggle']")) {
state.autoSaveHistory = event.target.checked;
render({ preserveFocus: true });
document.querySelector("[data-action='auto-save-toggle']")?.focus();
} else if (event.target.matches("[data-action='save-question-setting']")) {
state.saveQuestion = event.target.checked;
} else if (event.target.matches("[data-action='save-explanation-setting']")) {
state.saveExplanation = event.target.checked;
} else if (event.target.matches("[data-action='save-action-setting']")) {
state.saveAction = event.target.checked;
}
2026-08-04 17:54:15 +08:00
});
document.addEventListener("click", (event) => {
const actionTarget = event.target.closest("[data-action]");
if (!actionTarget) return;
const action = actionTarget.dataset.action;
if (action === "dismiss-modal" && event.target.closest("[data-modal-sheet]")) return;
if (action === "back") goBack();
else if (action === "begin") {
2026-08-04 23:00:05 +08:00
state.questionOrigin = state.screen;
2026-08-04 17:54:15 +08:00
state.screen = "question";
render();
2026-08-04 23:00:05 +08:00
} else if (action === "open-history") {
state.screen = "history";
render();
} else if (action === "open-settings") {
state.settingsOrigin = state.screen === "settings" ? "home" : state.screen;
state.screen = "settings";
render();
} else if (action === "open-history-detail") {
state.selectedRecordId = actionTarget.dataset.recordId;
state.screen = "history-detail";
render();
2026-08-04 17:54:15 +08:00
} else if (action === "skip-question") {
2026-08-04 23:00:05 +08:00
prepareNewSession();
2026-08-04 17:54:15 +08:00
state.question = "未记录具体问题(仅作自我观察)";
state.lines = [];
state.coins = [null, null, null];
state.screen = "casting";
render();
} else if (action === "start-casting") {
if (!state.question.trim()) return;
2026-08-04 23:00:05 +08:00
prepareNewSession();
2026-08-04 17:54:15 +08:00
state.lines = [];
state.coins = [null, null, null];
state.screen = "casting";
render();
} else if (action === "toggle-coin") {
const index = Number(actionTarget.dataset.index);
const current = state.coins[index];
state.coins[index] = current === null || current === 3 ? 2 : 3;
render({ preserveFocus: true });
document.querySelector(`[data-action='toggle-coin'][data-index='${index}']`)?.focus();
} else if (action === "confirm-line") {
if (!state.coins.every((value) => value !== null)) return;
state.lines.push(state.coins.reduce((total, value) => total + value, 0));
state.coins = [null, null, null];
if (state.lines.length === 6) {
2026-08-04 23:00:05 +08:00
if (state.autoSaveHistory) persistCurrentSession("automatic");
2026-08-04 17:54:15 +08:00
state.screen = "result";
}
render();
} else if (action === "undo-line") {
state.lines.pop();
state.coins = [null, null, null];
render();
showToast("已撤销上一爻");
} else if (action === "open-interpretation") {
state.modal = "interpretation";
renderModal();
} else if (action === "choose-ai") {
state.modal = "consent";
state.consent = false;
renderModal();
} else if (action === "choose-local") {
state.modal = null;
state.explanationSource = "local";
2026-08-04 23:00:05 +08:00
state.explanationGenerated = true;
attachExplanationToCurrentSession();
2026-08-04 17:54:15 +08:00
state.screen = "explanation";
render();
} else if (action === "confirm-ai") {
if (!state.consent) return;
beginAiExplanation();
} else if (action === "retry-ai") {
state.aiError = false;
state.modal = "consent";
state.consent = false;
renderModal();
2026-08-04 23:00:05 +08:00
} else if (action === "save-current") {
persistCurrentSession();
render();
showToast("本次记录已保存到本机");
} else if (action === "remove-current-record") {
if (!state.saved) return;
state.selectedRecordId = state.currentRecordId;
state.deleteReturnScreen = state.screen;
state.modal = "delete";
renderModal();
} else if (action === "delete-record") {
state.deleteReturnScreen = "history";
state.modal = "delete";
renderModal();
} else if (action === "confirm-delete") {
const deletedCurrentSession = state.selectedRecordId === state.currentRecordId;
state.historyRecords = state.historyRecords.filter((record) => record.id !== state.selectedRecordId);
state.selectedRecordId = null;
state.modal = null;
if (deletedCurrentSession) {
state.saved = false;
state.currentSessionOptedOut = true;
state.currentSaveMode = null;
}
state.screen = state.deleteReturnScreen;
render();
showToast("记录已从本机删除");
} else if (action === "open-clear-all") {
if (!state.historyRecords.length) return;
state.modal = "clear-all";
renderModal();
} else if (action === "confirm-clear-all") {
const includedCurrentSession = Boolean(currentSessionRecord());
state.historyRecords = [];
state.selectedRecordId = null;
state.modal = null;
if (includedCurrentSession) {
state.saved = false;
state.currentSessionOptedOut = true;
state.currentSaveMode = null;
}
render();
showToast("已清空全部本机记录");
2026-08-04 17:54:15 +08:00
} else if (action === "restart") {
state.question = "";
state.lines = [];
state.coins = [null, null, null];
2026-08-04 23:00:05 +08:00
prepareNewSession();
state.questionOrigin = state.historyRecords.length ? "home" : "welcome";
state.screen = "question";
2026-08-04 17:54:15 +08:00
render();
} else if (action === "back-to-result") {
state.screen = "result";
render();
} else if (action === "open-method") {
state.modal = "method";
renderModal();
} else if (action === "close-modal" || action === "dismiss-modal") {
closeModal();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && state.modal) closeModal();
});
function seedPreview() {
const view = new URLSearchParams(window.location.search).get("view");
if (!view || view === "welcome") return;
if (view === "question") {
state.screen = "question";
state.question = SAMPLE_QUESTION;
return;
}
2026-08-04 23:00:05 +08:00
if (["home", "home-empty", "history", "history-empty", "history-detail", "delete", "settings", "settings-off", "clear-all"].includes(view)) {
state.historyRecords = view === "home-empty" || view === "history-empty" ? [] : sampleHistory();
state.screen = view.startsWith("home")
? "home"
: view === "history-detail" || view === "delete"
? "history-detail"
: view === "settings" || view === "settings-off" || view === "clear-all"
? "settings"
: "history";
state.selectedRecordId = state.historyRecords[0]?.id ?? null;
if (view === "settings-off") state.autoSaveHistory = false;
if (view === "delete") state.modal = "delete";
if (view === "clear-all") state.modal = "clear-all";
return;
}
2026-08-04 17:54:15 +08:00
state.question = SAMPLE_QUESTION;
if (view === "casting") {
state.screen = "casting";
state.lines = [9, 8];
state.coins = [2, 3, null];
return;
}
state.lines = view === "static" ? [7, 7, 7, 7, 7, 7] : [...SAMPLE_LINES];
state.screen = "result";
2026-08-04 23:00:05 +08:00
if (view === "result-not-saved") state.autoSaveHistory = false;
if (state.autoSaveHistory) persistCurrentSession("automatic");
2026-08-04 17:54:15 +08:00
if (view === "consent") state.modal = "consent";
else if (view === "explanation") {
state.screen = "explanation";
state.explanationSource = "ai";
2026-08-04 23:00:05 +08:00
state.explanationGenerated = true;
attachExplanationToCurrentSession();
2026-08-04 17:54:15 +08:00
} else if (view === "local") {
state.screen = "explanation";
state.explanationSource = "local";
2026-08-04 23:00:05 +08:00
state.explanationGenerated = true;
attachExplanationToCurrentSession();
2026-08-04 17:54:15 +08:00
} else if (view === "error") {
state.aiError = true;
}
}
seedPreview();
render();
})();