Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/components/CanvasNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,9 @@ export function faceText(body: string): string {

export function ConceptCard({ data, selected }: NodeProps<CardNode>) {
const n = data.concept;
// Weighted AI cards (weightActions) only count core/hard answers as mastery; manual boards keep
// the learner's own 「我能解释」 mark as the seal.
const mastered = data.weightActions ? cardMastered(n) : n.status === "explained";
const edits = useContext(AgentEdits);
// Stamp only when the status changes while mounted, not on first paint.
const first = useRef(n.status);
Expand All @@ -509,11 +512,11 @@ export function ConceptCard({ data, selected }: NodeProps<CardNode>) {
{data.generating && <BorderBeam />}
{data.noteOpen && <NoteTab />}
<Ports linkMode={data.linkMode} />
{cardMastered(n) && data.station === undefined && <Seal light={n.seal === "probe"} />}
{mastered && (!data.weightActions || data.station === undefined) && <Seal light={n.seal === "probe"} />}
{data.preview === "replace" && <span className="pv-tag">待替换</span>}
{data.station !== undefined && data.preview !== "remove" && (
<span className={`station ${data.isNext ? "next" : ""} ${cardMastered(n) ? "sealed" : ""}`} aria-label={`第 ${data.station} 站`}>
{n.status !== "unseen" && <i className={`station-dot ${cardMastered(n) ? "mastered" : "read"}`} aria-label={cardMastered(n) ? "掌握" : "读过"} />}
<span className={`station ${data.isNext ? "next" : ""} ${mastered ? "sealed" : ""}`} aria-label={`第 ${data.station} 站`}>
{n.status !== "unseen" && <i className={`station-dot ${mastered ? "mastered" : "read"}`} aria-label={mastered ? "掌握" : "读过"} />}
<span>第 {data.station} 站</span>
</span>
)}
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/electron-transfer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,10 @@ test("desktop import restores SQLite histories; native exports and reopening pre
}),
[imported!.id, board.notes[0].id],
);
expect(restored.board!.nodes).toEqual(board.nodes);
// Restored boards pass the schema, which fills the goal-weights default weight.
expect(restored.board!.nodes).toEqual(
board.nodes.map((n: any) => ({ ...n, weight: n.weight ?? "background" })),
);
expect(restored.history.messages).toHaveLength(242);
expect(restored.history.messages[0].text).toBe("桌面共享记录 0");
expect(restored.history.running).toBe(false);
Expand Down
13 changes: 9 additions & 4 deletions tests/e2e/flow-p2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ async function newQuestion(page: Page, topic: string) {
await page.getByRole("button", { name: "开始探索", exact: true }).click();
}

test("摸底 v2: brackets the edge without levels or marks; stations below the start get the light seal", async ({ page }) => {
// spec-v2 (goal-weights): a new question now opens the chat kickoff (产出物/成功标准/约束 3–5 题),
// not the 摸底 probe, so this entry path is no longer reachable from 新问题. Kept for reference until the
// probe code is either rewired or removed.
test.skip("摸底 v2: brackets the edge without levels or marks; stations below the start get the light seal", async ({ page }) => {
await installFakeAtlas(page);
await newQuestion(page, "重疾险有哪些重要条款?");
const probe = page.locator(".drawer").getByRole("region", { name: "摸底" });
Expand Down Expand Up @@ -110,7 +113,7 @@ test("a light-sealed stop can be rechecked; failing it removes the seal and queu
await stop.getByRole("button", { name: "复查一下" }).click();
await stop.getByRole("radio", { name: /不赔/ }).click();
await stop.getByRole("button", { name: /提交/ }).click();
await expect(page.getByText(/去掉了浅印/)).toBeVisible();
await expect(page.getByText(/这一站排在下一步之后/)).toBeVisible();
await expect
.poll(async () => {
const d = await disk(page);
Expand All @@ -119,7 +122,9 @@ test("a light-sealed stop can be rechecked; failing it removes the seal and queu
.toEqual(["none", "c-when-not,c-what,c-how,c-money"]);
});

test("stuck after two wrong answers: three exits; 换个类比 goes back to 学, 往下拆 drafts two children", async ({ page }) => {
// spec-v2 (goal-weights): the first wrong answer now promotes the card one weight level and swaps in
// the choice footer (see flow.spec.ts), so the old two-wrong-answers 卡住了 exits are no longer reached.
test.skip("stuck after two wrong answers: three exits; 换个类比 goes back to 学, 往下拆 drafts two children", async ({ page }) => {
const b = bare();
Object.assign(b.nodes.find((n: any) => n.id === "c-when-not"), { status: "exploring", beat: "test", fails: 1 });
await installFakeAtlas(page, true, "web", [b]);
Expand Down Expand Up @@ -180,7 +185,7 @@ test("稍后 puts the stuck stop off to the end of the route", async ({ page })
await page.getByRole("group", { name: "卡住了" }).getByRole("button", { name: /稍后/ }).click();
await expect(bar).toContainText("下一步 · 第 2 站");
await expect(bar).not.toContainText("等待期");
await expect(card(page, "c-when-not").locator(".station")).toHaveText("4");
await expect(card(page, "c-when-not").locator(".station")).toHaveText("第 4 站");
});

test("合起来讲: with every stop sealed, the root asks for the wrap-up; the stations it reaches light up", async ({ page }) => {
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/goal-weights.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { mkdirSync } from "node:fs";
import { test, expect, type Page } from "@playwright/test";
import { installFakeAtlas } from "./fake-atlas";
import { gotoApp, reloadApp } from "./ready";
import { fixtureGoal, goalBoard } from "../goal-fixtures";
import { createEmptyBoard } from "../../src/model/boards";

const shots = "/workspace/studyboard-goal/v2/shots";
// Review screenshots; CI has no /workspace, so default into test-results unless GOAL_SHOTS is set.
const shots = process.env.GOAL_SHOTS ?? "test-results/goal-weights-shots";
const card = (page: Page, id: string) =>
page.locator(`.react-flow__node[data-id="${id}"]`);
const disk = (page: Page) =>
Expand All @@ -28,6 +30,7 @@ async function screenshot(page: Page, name: string) {
await page
.locator("textarea")
.evaluateAll((els) => els.forEach((el) => el.blur()));
mkdirSync(shots, { recursive: true });
await page.screenshot({
path: `${shots}/${name}.png`,
animations: "disabled",
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/sessions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ test("sidebar conversations stay ordered, persist and reopen on their cards", as
await expect(drawer.locator(".msg.user")).toHaveText("赔付次数怎么算?");
await expect(card(page, "c-how")).toHaveClass(/selected/);
await drawer.getByRole("button", { name: "关闭", exact: true }).click();
await card(page, "c-how").click();
// Asking on c-how promotes it (goal-weights), so its face now has a weight footer; click the title.
await card(page, "c-how").locator("h3").click();
await expect(drawer.locator(".msg.user")).toHaveText("间隔期是多久?");
});

Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/transfer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ test("a shared file restores complete conversations as a new board, survives rel
JSON.parse(localStorage.getItem("studyboard-preview-boards-v1")!).at(-1),
);
expect(imported.id).not.toBe(original.id);
expect(imported.nodes).toEqual(original.nodes);
// Import validates through the board schema, which fills the goal-weights default weight.
expect(imported.nodes).toEqual(
original.nodes.map((n: any) => ({ ...n, weight: n.weight ?? "background" })),
);
expect(imported.notes[0].messages).toHaveLength(200);
await reloadApp(page);
await page
Expand Down
Loading