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
4 changes: 4 additions & 0 deletions packages/agent/src/server/sse/wire-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* 생략한다 — summary는 항상 best-effort.
*/

import { formatLatex } from "../../tools/latex-formatter.js";
import type {
GateResult,
GeneratedProblem,
Expand Down Expand Up @@ -234,10 +235,13 @@ export function toWireResultProblem(
problem: GeneratedProblem,
verification: Verification,
): WireResultProblem {
const solution = problem.proposed_solution_trace.trim();
return {
id: problem.candidate_id,
question_latex: problem.question_text,
answer_latex: problem.expected_answer,
// 풀이 trace 는 sympy 표기 그대로 오므로 표현 경계에서 LaTeX 로 변환
...(solution.length === 0 ? {} : { explanation_latex: formatLatex(solution) }),
isomorphism: problem.mode,
preserved_dimensions: preservedDimensions(problem),
source_refs: problem.source_refs,
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/tests/backend-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ describe("SSE wire adapter", () => {
expect(wire.data[0]?.id).toBe(problem.candidate_id);
expect(wire.data[0]?.verification_status).toBe("pass");
expect(wire.data[0]?.source_refs).toEqual(["seed-9수02-09-001"]);
expect(wire.data[0]?.explanation_latex).toBeUndefined();
expect(wire.data[0]?.explanation_latex).toBe("(x - 2)(x - 3) = 0");
expect(wire.data[0]?.preserved_dimensions).toEqual([
"이차식을 인수분해하여 해를 구한다",
]);
Expand Down
18 changes: 18 additions & 0 deletions packages/agent/tests/wire-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ describe("toWireResultProblem — verification + provenance projection", () => {
"이차식을 인수분해하여 해를 구한다",
]);
expect(wire.source_refs).toEqual(["seed-9수02-09-001"]);
expect(wire.explanation_latex).toBe("(x - 2)(x - 3) = 0");
});

it("formats the solution trace to KaTeX-safe LaTeX in explanation_latex", () => {
const problem: GeneratedProblem = {
...baseProblem,
proposed_solution_trace: "판별식: x**2 - 5*x + 6 = 0, sqrt(4) = 2",
};
const wire = toWireResultProblem(problem, makeVerification());
expect(wire.explanation_latex).toBe("판별식: x^{2} - 5 x + 6 = 0, \\sqrt{4} = 2");
});

it("omits explanation_latex when the solution trace is blank", () => {
const problem: GeneratedProblem = {
...baseProblem,
proposed_solution_trace: " ",
};
const wire = toWireResultProblem(problem, makeVerification());
expect(wire.explanation_latex).toBeUndefined();
});

Expand Down
54 changes: 54 additions & 0 deletions packages/web/__tests__/latex-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import {
segmentAuto,
splitDelimited,
type Segment,
} from "../components/math/latex-renderer";
Expand Down Expand Up @@ -126,3 +127,56 @@ describe("splitDelimited", () => {
expect(splitDelimited("\\$ \\[ x \\]")).toEqual(expected);
});
});

describe("segmentAuto", () => {
it("keeps pure Korean prose as a single text segment", () => {
const source = "어느 축구 동호회에서 12명의 선수를 배치하려고 한다.";
const expected: Segment[] = [{ kind: "text", value: source }];
expect(segmentAuto(source)).toEqual(expected);
});

it("extracts an undelimited equation run between Korean prose", () => {
expect(segmentAuto("방정식 x^{2} - 5 x + 6 = 0 의 해를 구하시오.")).toEqual([
{ kind: "text", value: "방정식 " },
{ kind: "inline", latex: "x^{2} - 5 x + 6 = 0" },
{ kind: "text", value: " 의 해를 구하시오." },
]);
});

it("extracts a LaTeX command run like \\sqrt{7}", () => {
expect(segmentAuto("한 변의 길이가 \\sqrt{7} 인 정사각형")).toEqual([
{ kind: "text", value: "한 변의 길이가 " },
{ kind: "inline", latex: "\\sqrt{7}" },
{ kind: "text", value: " 인 정사각형" },
]);
});

it("leaves enumerations like 'A, B, C, D' and bare numbers as text", () => {
const source = "학생 A, B, C, D 4명을 앞줄에 세운다. 답은 3456 이다.";
const expected: Segment[] = [{ kind: "text", value: source }];
expect(segmentAuto(source)).toEqual(expected);
});

it("splits trailing sentence punctuation out of a math run", () => {
expect(segmentAuto("따라서 x = 3, 검산 끝.")).toEqual([
{ kind: "text", value: "따라서 " },
{ kind: "inline", latex: "x = 3" },
{ kind: "text", value: ", 검산 끝." },
]);
});

it("delegates to splitDelimited when $ delimiters are present", () => {
expect(segmentAuto("답은 $x = 3$ 이다")).toEqual([
{ kind: "text", value: "답은 " },
{ kind: "inline", latex: "x = 3" },
{ kind: "text", value: " 이다" },
]);
});

it("preserves newlines in solution traces as text", () => {
const segments = segmentAuto("1단계: 경우를 나눈다\n2단계: 곱한다");
expect(segments).toEqual([
{ kind: "text", value: "1단계: 경우를 나눈다\n2단계: 곱한다" },
]);
});
});
6 changes: 3 additions & 3 deletions packages/web/app/app/new/export/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from "next/link";
import { useEffect, useMemo, useRef, useState } from "react";
import { LatexRenderer } from "@/components/math/latex-renderer";
import { LatexAuto } from "@/components/math/latex-renderer";
import { type Grade, type SchoolLevel, type Topic, gradeLabel } from "../topic/data";
import { verificationStorageKey } from "@/lib/verification-storage-key";
import type { ResultProblem } from "../result/types";
Expand Down Expand Up @@ -136,7 +136,7 @@ function ExamSheet({
<li key={p.id} className="exam-problem">
<span className="num">{i + 1}.</span>
<span className="body">
<LatexRenderer latex={p.questionLatex} />
<LatexAuto source={p.questionLatex} />
</span>
</li>
))}
Expand All @@ -148,7 +148,7 @@ function ExamSheet({
{problems.map((p, i) => (
<li key={p.id}>
<span>{i + 1}. </span>
<LatexRenderer latex={p.answerLatex} />
<LatexAuto source={p.answerLatex} />
</li>
))}
</ol>
Expand Down
2 changes: 1 addition & 1 deletion packages/web/app/app/new/result/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type ResultProblem = {
status: ResultStatus;
questionLatex: string;
answerLatex: string;
solutionLatex: string;
solutionLatex: string | null;
failReason: string | null;
generationModel?: string;
refinedBy?: string[];
Expand Down
78 changes: 25 additions & 53 deletions packages/web/app/app/new/result/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { LatexRenderer } from "@/components/math/latex-renderer";
import { LatexAuto } from "@/components/math/latex-renderer";
import { type Grade, type SchoolLevel, type Topic, gradeLabel } from "../topic/data";
import { verificationStorageKey } from "@/lib/verification-storage-key";
import type { ResultProblem } from "./types";
Expand Down Expand Up @@ -142,7 +142,7 @@ function toResultProblem(problem: StoredProblem, index: number): ResultProblem {
status,
questionLatex: problem.question_latex,
answerLatex: problem.answer_latex,
solutionLatex: problem.explanation_latex ?? "검증 파이프라인에서 생성된 문항입니다.",
solutionLatex: problem.explanation_latex ?? null,
failReason: status === "fail" ? "검증 실패 — 채택할 수 없습니다." : null,
generationModel: problem.generation_model,
refinedBy: problem.refined_by,
Expand Down Expand Up @@ -309,7 +309,13 @@ export function ResultView({
</p>

<div className="filter-row" role="toolbar" aria-label="결과 필터">
{filters.map((f) => {
{filters
.filter(
(f) =>
f.value === "all" ||
displayProblems.some((p) => matchesFilter(p, f.value)),
)
.map((f) => {
const active = filter === f.value;
return (
<button
Expand Down Expand Up @@ -390,39 +396,6 @@ export function ResultView({
{isAdopted ? "★" : "☆"}
</span>
</button>
<button
type="button"
className="btn-icon-circular"
aria-label="다시 생성"
disabled
aria-describedby={`card-${p.id}-v2note`}
>
<span aria-hidden="true">↻</span>
</button>
<button
type="button"
className="btn-icon-circular"
aria-label="수정"
disabled
aria-describedby={`card-${p.id}-v2note`}
>
<span aria-hidden="true">✎</span>
</button>
<button
type="button"
className="btn-icon-circular"
aria-label="폐기"
disabled
aria-describedby={`card-${p.id}-v2note`}
>
<span aria-hidden="true">×</span>
</button>
<span
id={`card-${p.id}-v2note`}
className="sr-only"
>
v2 에서 도입 예정 — 1차 MVP 에서는 비활성.
</span>
</div>
</header>

Expand All @@ -439,30 +412,29 @@ export function ResultView({
) : null}

<div className="card-body">
<div className="formula-stage">
<LatexRenderer
latex={p.questionLatex}
block
/>
<div className="formula-stage question-stage">
<LatexAuto source={p.questionLatex} />
</div>
</div>

<div className="card-meta">
<div className="answer">
<span className="answer-label">답</span>
<LatexRenderer latex={p.answerLatex} />
<LatexAuto source={p.answerLatex} />
</div>
<details className="disclosure-row solution-disclosure">
<summary>
<span>풀이 보기</span>
<span className="chevron" aria-hidden="true">
</span>
</summary>
<div className="solution-body">
<LatexRenderer latex={p.solutionLatex} />
</div>
</details>
{p.solutionLatex !== null ? (
<details className="disclosure-row solution-disclosure">
<summary>
<span>풀이 보기</span>
<span className="chevron" aria-hidden="true">
</span>
</summary>
<div className="solution-body">
<LatexAuto source={p.solutionLatex} />
</div>
</details>
) : null}
</div>
</article>
);
Expand Down
6 changes: 3 additions & 3 deletions packages/web/app/app/new/verify/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import Link from "next/link";
import { useEffect, useMemo, useRef, useState } from "react";
import { LatexRenderer } from "@/components/math/latex-renderer";
import { LatexAuto } from "@/components/math/latex-renderer";
import {
type Grade,
type SchoolLevel,
Expand Down Expand Up @@ -355,9 +355,9 @@ export function VerifyView({ schoolLevel, grade, topic, mode, srcRef }: Props) {

{showPreview && stream.previewLatex !== null ? (
<div className="formula-stage-wrap">
<div className="formula-stage">
<div className="formula-stage question-stage">
<span className="caption">CANDIDATE PREVIEW</span>
<LatexRenderer latex={stream.previewLatex} block />
<LatexAuto source={stream.previewLatex} />
</div>
</div>
) : null}
Expand Down
17 changes: 17 additions & 0 deletions packages/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1647,6 +1647,20 @@
margin-bottom: 12px;
}

/* question-stage — 산문 문항용 formula-stage 변형.
* 한글 문장이 주가 되므로 가운데 정렬·mono·break-all 을 풀고
* 자연스러운 줄바꿈(keep-all)으로 읽히게 한다. */
.formula-stage.question-stage {
display: block;
font-family: var(--font-sans);
font-size: 15.5px;
line-height: 1.7;
text-align: left;
word-break: keep-all;
overflow-wrap: anywhere;
overflow-x: hidden;
}

/* result-card — DESIGN.md {component.result-card} / {component.result-card-failed} */
.result-grid {
display: grid;
Expand Down Expand Up @@ -1822,6 +1836,9 @@
font-size: 14px;
line-height: 1.55;
color: var(--color-ink-2);
white-space: pre-wrap;
word-break: keep-all;
overflow-wrap: anywhere;
}

/* S6 — export layout */
Expand Down
Loading
Loading